2019-05-02 22:45:40 +00:00
|
|
|
|
/*
|
|
|
|
|
* 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.Diagnostics;
|
|
|
|
|
using System.Windows.Media;
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
2024-07-06 21:54:09 +00:00
|
|
|
|
// Spaces and hyphens are different. For example, if width is 10,
|
|
|
|
|
// "long words<space>more words" becomes:
|
|
|
|
|
// 0123456789
|
|
|
|
|
// long words
|
|
|
|
|
// more words
|
|
|
|
|
// However, "long words-more words" becomes:
|
|
|
|
|
// long
|
|
|
|
|
// words-more
|
|
|
|
|
// words
|
|
|
|
|
// because the hyphen is retained but the space is discarded.
|
|
|
|
|
|
2019-07-20 20:28:10 +00:00
|
|
|
|
namespace SourceGen {
|
2019-05-02 22:45:40 +00:00
|
|
|
|
/// <summary>
|
2024-07-04 23:04:33 +00:00
|
|
|
|
/// <para>Representation of a multi-line comment, which is a string plus some format options.
|
|
|
|
|
/// Used for long comments and notes.</para>
|
2019-05-02 22:45:40 +00:00
|
|
|
|
///
|
2024-07-04 23:04:33 +00:00
|
|
|
|
/// <para>Instances are effectively immutable, as the text and options can't be modified
|
|
|
|
|
/// after the object is created. The object does cache the result of the last FormatText()
|
|
|
|
|
/// call, which is determined in part by the Formatter argument, which can change between
|
|
|
|
|
/// calls.</para>
|
2019-05-02 22:45:40 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
public class MultiLineComment {
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Unformatted text.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string Text { get; private set; }
|
|
|
|
|
|
2024-07-04 23:04:33 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// True if this uses "fancy" formatting. If set, the BoxMode and MaxWidth properties
|
|
|
|
|
/// are ignored.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool IsFancy { get; private set; }
|
|
|
|
|
|
2019-05-02 22:45:40 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set to true to render text surrounded by a box of ASCII characters.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool BoxMode { get; private set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Maximum line width. Box mode effectively reduces this by four.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public int MaxWidth { get; private set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Background color for notes.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Color BackgroundColor { get; private set; }
|
|
|
|
|
|
2024-07-04 17:32:24 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Box character to use for "basic" formatting.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private const char BASIC_BOX_CHAR = '*';
|
|
|
|
|
|
2024-07-04 23:04:33 +00:00
|
|
|
|
private const int DEFAULT_WIDTH = 80;
|
|
|
|
|
private const int MIN_WIDTH = 8;
|
2024-07-06 21:54:09 +00:00
|
|
|
|
private const int MAX_WIDTH = 128;
|
|
|
|
|
private const string SPACES = // MAX_WIDTH spaces
|
|
|
|
|
" " +
|
|
|
|
|
" ";
|
2024-07-04 23:04:33 +00:00
|
|
|
|
|
2019-05-02 22:45:40 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
2024-07-04 23:04:33 +00:00
|
|
|
|
/// Constructor. By default, comments use basic formatting, have a basic-mode max
|
|
|
|
|
/// width of 80, and aren't boxed.
|
2019-05-02 22:45:40 +00:00
|
|
|
|
/// </summary>
|
2024-07-04 23:04:33 +00:00
|
|
|
|
/// <remarks>
|
|
|
|
|
/// We'd actually prefer to have fancy formatting be the default, but that does the
|
|
|
|
|
/// wrong thing when deserializing older projects.
|
|
|
|
|
/// </remarks>
|
2019-05-02 22:45:40 +00:00
|
|
|
|
/// <param name="text">Unformatted comment text.</param>
|
|
|
|
|
public MultiLineComment(string text) {
|
|
|
|
|
Debug.Assert(text != null); // empty string is okay
|
|
|
|
|
Text = text;
|
2024-07-04 23:04:33 +00:00
|
|
|
|
IsFancy = false;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
BoxMode = false;
|
2024-07-04 23:04:33 +00:00
|
|
|
|
MaxWidth = DEFAULT_WIDTH;
|
Improve save & restore of top line
Whenever the display list gets regenerated, we need to restore the
code list view scroll position to the previous location in the file.
This gets tricky when multiple lines are appearing or disappearing.
We were saving the file offset of the line, but that works poorly
when there's a multi-line comment associated with that offset,
because we end up scrolling to the top of the comment whenever any
part of the comment is at the top of the screen.
We now track the file offset and the number of lines we were from
the top of that offset's content. This works well unless we remove
a lot of lines. If the adjusted line index would put us into a
different file offset, we punt and just scroll to the top of the item.
Also, fix a crasher in Edit Note.
Also, fix behavior when the list shrinks while a line near the end
of the file is selected.
Also, change a few instances of "Color.FromArgb(0,0,0,0)" to use a
common constant.
2019-07-17 20:47:43 +00:00
|
|
|
|
BackgroundColor = CommonWPF.Helper.ZeroColor;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-04 23:04:33 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Constructor. Used when creating an empty MLC for editing.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="isFancy">True if we want to be in "fancy" mode initially.</param>
|
|
|
|
|
public MultiLineComment(bool isFancy) : this(string.Empty) {
|
|
|
|
|
IsFancy = isFancy;
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 22:45:40 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Constructor. Used for long comments.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="text">Unformatted text.</param>
|
2024-07-04 23:04:33 +00:00
|
|
|
|
/// <param name="isFancy">True if we're using fancy format mode.</param>
|
|
|
|
|
/// <param name="boxMode">For basic mode, set to true to enable box mode.</param>
|
|
|
|
|
/// <param name="maxWidth">For basic mode, maximum line width.</param>
|
|
|
|
|
public MultiLineComment(string text, bool isFancy, bool boxMode, int maxWidth)
|
|
|
|
|
: this(text) {
|
|
|
|
|
if (maxWidth < MIN_WIDTH) {
|
|
|
|
|
Debug.Assert(false, "unexpectedly small max width");
|
|
|
|
|
maxWidth = MIN_WIDTH;
|
|
|
|
|
}
|
|
|
|
|
IsFancy = isFancy;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
BoxMode = boxMode;
|
|
|
|
|
MaxWidth = maxWidth;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Constructor. Used for notes.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="text">Unformatted text.</param>
|
|
|
|
|
/// <param name="bkgndColor">Background color.</param>
|
|
|
|
|
public MultiLineComment(string text, Color bkgndColor) : this(text) {
|
|
|
|
|
BackgroundColor = bkgndColor;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-04 23:04:33 +00:00
|
|
|
|
private List<string> mPreviousRender = null;
|
|
|
|
|
private Asm65.Formatter mPreviousFormatter = null;
|
|
|
|
|
private string mPreviousPrefix = null;
|
|
|
|
|
|
2019-05-02 22:45:40 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Generates one or more lines of formatted text.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="formatter">Formatter, with comment delimiters.</param>
|
|
|
|
|
/// <param name="textPrefix">String to prepend to text before formatting. If this
|
|
|
|
|
/// is non-empty, comment delimiters aren't emitted. (Used for notes.)</param>
|
2024-07-04 23:04:33 +00:00
|
|
|
|
/// <returns>List of formatted strings. Do not modify the list.</returns>
|
2019-05-02 22:45:40 +00:00
|
|
|
|
public List<string> FormatText(Asm65.Formatter formatter, string textPrefix) {
|
2024-07-04 23:04:33 +00:00
|
|
|
|
if (mPreviousRender != null && formatter == mPreviousFormatter &&
|
|
|
|
|
textPrefix == mPreviousPrefix) {
|
|
|
|
|
// We rendered this with the same formatter before. Return the list. It would
|
|
|
|
|
// be safer to clone the list, but I'm not expecting the caller to edit it.
|
|
|
|
|
return mPreviousRender;
|
|
|
|
|
}
|
|
|
|
|
List<string> lines;
|
2024-07-07 23:02:46 +00:00
|
|
|
|
try {
|
|
|
|
|
if (IsFancy) {
|
|
|
|
|
Debug.Assert(string.IsNullOrEmpty(textPrefix));
|
|
|
|
|
lines = FormatFancyText(formatter);
|
|
|
|
|
} else {
|
|
|
|
|
lines = FormatSimpleText(formatter, textPrefix);
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ex) {
|
|
|
|
|
Debug.WriteLine("FormatText failed: " + ex);
|
|
|
|
|
lines = new List<string>();
|
|
|
|
|
lines.Add("Internal error: " + ex.Message);
|
2024-07-04 23:04:33 +00:00
|
|
|
|
}
|
|
|
|
|
// Cache result.
|
|
|
|
|
mPreviousRender = lines;
|
|
|
|
|
mPreviousFormatter = formatter;
|
|
|
|
|
mPreviousPrefix = textPrefix;
|
|
|
|
|
return lines;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Generates one or more lines of formatted text, using the basic formatter.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="formatter">Formatter, with comment delimiters.</param>
|
|
|
|
|
/// <param name="textPrefix">String to prepend to text before formatting. If this
|
|
|
|
|
/// is non-empty, comment delimiters aren't emitted. (Used for notes.)</param>
|
|
|
|
|
/// <returns>List of formatted strings.</returns>
|
|
|
|
|
private List<string> FormatSimpleText(Asm65.Formatter formatter, string textPrefix) {
|
2024-06-24 23:36:27 +00:00
|
|
|
|
const char spcRep = '\u2219'; // BULLET OPERATOR
|
2019-05-02 22:45:40 +00:00
|
|
|
|
string workString = string.IsNullOrEmpty(textPrefix) ? Text : textPrefix + Text;
|
|
|
|
|
List<string> lines = new List<string>();
|
2024-07-06 21:54:09 +00:00
|
|
|
|
bool debugMode = formatter.DebugLongComments;
|
|
|
|
|
|
|
|
|
|
if (MaxWidth > MAX_WIDTH) {
|
|
|
|
|
lines.Add("!Bad MaxWidth!");
|
|
|
|
|
return lines;
|
|
|
|
|
}
|
2019-05-02 22:45:40 +00:00
|
|
|
|
|
|
|
|
|
string linePrefix;
|
|
|
|
|
if (!string.IsNullOrEmpty(textPrefix)) {
|
2024-07-06 21:54:09 +00:00
|
|
|
|
// This is a Note, no comment delimiter needed.
|
2019-05-02 22:45:40 +00:00
|
|
|
|
linePrefix = string.Empty;
|
|
|
|
|
} else if (BoxMode) {
|
2024-07-04 17:32:24 +00:00
|
|
|
|
if (formatter.FullLineCommentDelimiterBase.Length == 1 &&
|
|
|
|
|
formatter.FullLineCommentDelimiterBase[0] == BASIC_BOX_CHAR) {
|
2024-07-06 21:54:09 +00:00
|
|
|
|
// Box char is same as comment delimiter, don't double-up.
|
2024-07-04 17:32:24 +00:00
|
|
|
|
linePrefix = string.Empty;
|
|
|
|
|
} else {
|
2024-07-06 21:54:09 +00:00
|
|
|
|
// Prefix with comment delimiter, but don't include optional space.
|
2024-07-04 17:32:24 +00:00
|
|
|
|
linePrefix = formatter.FullLineCommentDelimiterBase;
|
|
|
|
|
}
|
2019-05-02 22:45:40 +00:00
|
|
|
|
} else {
|
2024-07-06 21:54:09 +00:00
|
|
|
|
// No box, prefix every line with comment delimiter and optional space.
|
2024-07-04 17:32:24 +00:00
|
|
|
|
linePrefix = formatter.FullLineCommentDelimiterPlus;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder(MaxWidth);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
if (debugMode) {
|
2019-05-02 22:45:40 +00:00
|
|
|
|
for (int i = 0; i < MaxWidth; i++) {
|
|
|
|
|
sb.Append((i % 10).ToString());
|
|
|
|
|
}
|
|
|
|
|
lines.Add(sb.ToString());
|
|
|
|
|
sb.Clear();
|
|
|
|
|
}
|
2024-07-06 21:54:09 +00:00
|
|
|
|
string boxLine;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
if (BoxMode) {
|
|
|
|
|
for (int i = 0; i < MaxWidth - linePrefix.Length; i++) {
|
2024-07-04 17:32:24 +00:00
|
|
|
|
sb.Append(BASIC_BOX_CHAR);
|
2019-05-02 22:45:40 +00:00
|
|
|
|
}
|
|
|
|
|
boxLine = sb.ToString();
|
|
|
|
|
sb.Clear();
|
|
|
|
|
} else {
|
2024-07-06 21:54:09 +00:00
|
|
|
|
boxLine = null;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (BoxMode && workString.Length > 0) {
|
|
|
|
|
lines.Add(linePrefix + boxLine);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int lineWidth = BoxMode ?
|
|
|
|
|
MaxWidth - linePrefix.Length - 4 :
|
|
|
|
|
MaxWidth - linePrefix.Length;
|
2024-07-06 21:54:09 +00:00
|
|
|
|
Debug.Assert(lineWidth > 0);
|
2019-05-02 22:45:40 +00:00
|
|
|
|
int startIndex = 0;
|
|
|
|
|
int breakIndex = -1;
|
|
|
|
|
for (int i = 0; i < workString.Length; i++) {
|
|
|
|
|
if (workString[i] == '\r' || workString[i] == '\n') {
|
|
|
|
|
// explicit line break, emit line
|
|
|
|
|
string str = workString.Substring(startIndex, i - startIndex);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
if (debugMode) { str = str.Replace(' ', spcRep); }
|
2019-05-02 22:45:40 +00:00
|
|
|
|
if (BoxMode) {
|
2024-07-04 17:32:24 +00:00
|
|
|
|
if (str == "" + BASIC_BOX_CHAR) {
|
2019-05-02 22:45:40 +00:00
|
|
|
|
// asterisk on a line by itself means "output row of asterisks"
|
|
|
|
|
str = linePrefix + boxLine;
|
|
|
|
|
} else {
|
|
|
|
|
int padLen = lineWidth - str.Length;
|
2024-07-04 17:32:24 +00:00
|
|
|
|
str = linePrefix + BASIC_BOX_CHAR + " " + str +
|
2024-07-06 21:54:09 +00:00
|
|
|
|
SPACES.Substring(0, padLen + 1) + BASIC_BOX_CHAR;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
str = linePrefix + str;
|
|
|
|
|
}
|
|
|
|
|
lines.Add(str);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
// Eat the LF in CRLF.
|
|
|
|
|
if (workString[i] == '\r' && i < workString.Length - 1 &&
|
|
|
|
|
workString[i + 1] == '\n') {
|
2019-05-02 22:45:40 +00:00
|
|
|
|
i++;
|
|
|
|
|
}
|
|
|
|
|
startIndex = i + 1;
|
|
|
|
|
breakIndex = -1;
|
|
|
|
|
} else if (workString[i] == ' ') {
|
|
|
|
|
// can break on a space even if it's one char too far
|
|
|
|
|
breakIndex = i;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (i - startIndex >= lineWidth) {
|
2024-07-06 21:54:09 +00:00
|
|
|
|
// this character was one too many, break line at last break point
|
2019-05-02 22:45:40 +00:00
|
|
|
|
if (breakIndex <= 0) {
|
|
|
|
|
// no break found, just chop it
|
|
|
|
|
string str = workString.Substring(startIndex, i - startIndex);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
if (debugMode) { str = str.Replace(' ', spcRep); }
|
2019-05-02 22:45:40 +00:00
|
|
|
|
if (BoxMode) {
|
2024-07-04 17:32:24 +00:00
|
|
|
|
str = linePrefix + BASIC_BOX_CHAR + " " + str + " " + BASIC_BOX_CHAR;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
} else {
|
|
|
|
|
str = linePrefix + str;
|
|
|
|
|
}
|
|
|
|
|
lines.Add(str);
|
|
|
|
|
startIndex = i;
|
|
|
|
|
} else {
|
|
|
|
|
// Copy everything from start to break. If the break was a hyphen,
|
|
|
|
|
// we want to keep it.
|
|
|
|
|
int adj = 0;
|
|
|
|
|
if (workString[breakIndex] == '-') {
|
|
|
|
|
adj = 1;
|
|
|
|
|
}
|
|
|
|
|
string str = workString.Substring(startIndex,
|
|
|
|
|
breakIndex + adj - startIndex);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
if (debugMode) { str = str.Replace(' ', spcRep); }
|
2019-05-02 22:45:40 +00:00
|
|
|
|
if (BoxMode) {
|
|
|
|
|
int padLen = lineWidth - str.Length;
|
2024-07-04 17:32:24 +00:00
|
|
|
|
str = linePrefix + BASIC_BOX_CHAR + " " + str +
|
2024-07-06 21:54:09 +00:00
|
|
|
|
SPACES.Substring(0, padLen + 1) + BASIC_BOX_CHAR;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
} else {
|
|
|
|
|
str = linePrefix + str;
|
|
|
|
|
}
|
|
|
|
|
lines.Add(str);
|
|
|
|
|
startIndex = breakIndex + 1;
|
2019-11-02 02:39:08 +00:00
|
|
|
|
if (adj == 0 && startIndex < workString.Length &&
|
|
|
|
|
workString[startIndex] == ' ') {
|
|
|
|
|
// We broke on a space, and are now starting a line on a space,
|
|
|
|
|
// which looks weird (and happens commonly at the end of a
|
|
|
|
|
// sentence). Eat one more space.
|
|
|
|
|
startIndex++;
|
|
|
|
|
}
|
2019-05-02 22:45:40 +00:00
|
|
|
|
breakIndex = -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (workString[i] == '-') {
|
|
|
|
|
// can break on hyphen if it fits in line
|
|
|
|
|
breakIndex = i;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (startIndex < workString.Length) {
|
|
|
|
|
// Output remainder.
|
|
|
|
|
string str = workString.Substring(startIndex, workString.Length - startIndex);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
if (debugMode) { str = str.Replace(' ', spcRep); }
|
2019-05-02 22:45:40 +00:00
|
|
|
|
if (BoxMode) {
|
|
|
|
|
int padLen = lineWidth - str.Length;
|
2024-07-04 17:32:24 +00:00
|
|
|
|
str = linePrefix + BASIC_BOX_CHAR + " " + str +
|
2024-07-06 21:54:09 +00:00
|
|
|
|
SPACES.Substring(0, padLen + 1) + BASIC_BOX_CHAR;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
} else {
|
|
|
|
|
str = linePrefix + str;
|
|
|
|
|
}
|
|
|
|
|
lines.Add(str);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (BoxMode && workString.Length > 0) {
|
|
|
|
|
lines.Add(linePrefix + boxLine);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return lines;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-06 21:54:09 +00:00
|
|
|
|
#region Fancy
|
|
|
|
|
|
2024-07-07 15:48:58 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Input data source.
|
|
|
|
|
/// </summary>
|
2024-07-07 23:02:46 +00:00
|
|
|
|
/// <remarks>
|
|
|
|
|
/// <para>When we encounter a tag, we create a new DataSource that has the contents of
|
|
|
|
|
/// the tag in it, and skip over the full original extent. This is especially handy for
|
|
|
|
|
/// generated text, e.g. [url=x] link text, where the source isn't simply a subset of
|
|
|
|
|
/// the original.</para>
|
|
|
|
|
/// <para>Various bits of state are also stored here, so that we can prevent
|
|
|
|
|
/// inappropriate nesting and track options set by the format tags.</para>
|
|
|
|
|
/// </remarks>
|
2024-07-06 21:54:09 +00:00
|
|
|
|
private class DataSource {
|
|
|
|
|
private string mString;
|
|
|
|
|
private int mPosn;
|
|
|
|
|
|
2024-07-07 15:48:58 +00:00
|
|
|
|
public string Text => mString;
|
2024-07-07 23:02:46 +00:00
|
|
|
|
public char this[int i] {
|
|
|
|
|
get {
|
|
|
|
|
Debug.Assert(i >= 0 && i < mString.Length);
|
|
|
|
|
return mString[i];
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-06 21:54:09 +00:00
|
|
|
|
public int Posn { get { return mPosn; } set { mPosn = value; } }
|
|
|
|
|
public int Length => mString.Length;
|
2024-07-07 15:48:58 +00:00
|
|
|
|
public char CurChar => mString[mPosn];
|
|
|
|
|
|
|
|
|
|
// These are true if the text is appearing inside start/end tags.
|
|
|
|
|
public bool InBox { get; set; }
|
|
|
|
|
public bool InUrl { get; set; }
|
|
|
|
|
|
|
|
|
|
public bool InsideElement { get { return InBox || InsideNonBoxElement; } }
|
2024-07-07 23:02:46 +00:00
|
|
|
|
public bool InsideNonBoxElement { get { return InUrl; } }
|
2024-07-07 15:48:58 +00:00
|
|
|
|
|
2024-07-07 23:02:46 +00:00
|
|
|
|
// If true, don't prefix lines with the comment delimiter (used for [br]).
|
2024-07-07 15:48:58 +00:00
|
|
|
|
public bool SuppressPrefix { get; set; }
|
|
|
|
|
|
|
|
|
|
// True if using default char (comment delimiter) for boxes.
|
|
|
|
|
public bool BoxCharIsDefault { get; set; } = true;
|
|
|
|
|
public char BoxChar { get; set; } = '?';
|
2024-07-06 21:54:09 +00:00
|
|
|
|
|
2024-07-07 23:02:46 +00:00
|
|
|
|
// If true, don't inset text inside a box (used for [hr]).
|
|
|
|
|
public bool FullWidth { get; set; } = false;
|
|
|
|
|
|
2024-07-06 21:54:09 +00:00
|
|
|
|
public DataSource(string str, int posn, DataSource outer) {
|
|
|
|
|
mString = str;
|
|
|
|
|
mPosn = posn;
|
|
|
|
|
|
|
|
|
|
if (outer != null) {
|
|
|
|
|
// Inherit the values from the "outer" source.
|
2024-07-07 15:48:58 +00:00
|
|
|
|
InBox = outer.InBox;
|
|
|
|
|
InUrl = outer.InUrl;
|
|
|
|
|
SuppressPrefix = outer.SuppressPrefix;
|
|
|
|
|
BoxCharIsDefault = outer.BoxCharIsDefault;
|
|
|
|
|
BoxChar = outer.BoxChar;
|
2024-07-07 23:02:46 +00:00
|
|
|
|
FullWidth = outer.FullWidth;
|
2024-07-06 21:54:09 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-07 15:48:58 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns true if the string at the current position matches the argument. The
|
|
|
|
|
/// comparison is case-insensitive.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool Match(string str, int offset) {
|
|
|
|
|
if (mPosn + offset + str.Length > mString.Length) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < str.Length; i++) {
|
2024-07-07 23:02:46 +00:00
|
|
|
|
// Shouldn't need to worry about InvariantCultureIgnoreCase since this is
|
|
|
|
|
// only used for tags.
|
2024-07-07 15:48:58 +00:00
|
|
|
|
if (char.ToUpper(str[i]) != char.ToUpper(mString[mPosn + offset + i])) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2024-07-07 23:02:46 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns the position of the matching string. The search starts at the current
|
|
|
|
|
/// position, and is case-insensitive.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public int FindNext(string str) {
|
|
|
|
|
return Text.IndexOf(str, mPosn, StringComparison.InvariantCultureIgnoreCase);
|
|
|
|
|
}
|
2024-07-06 21:54:09 +00:00
|
|
|
|
}
|
|
|
|
|
private Stack<DataSource> mSourceStack = new Stack<DataSource>();
|
|
|
|
|
private StringBuilder mLineBuilder = new StringBuilder(MAX_WIDTH);
|
|
|
|
|
|
2024-07-07 15:48:58 +00:00
|
|
|
|
private const char DEFAULT_RULE_CHAR = '-';
|
2024-07-06 21:54:09 +00:00
|
|
|
|
|
|
|
|
|
private int mLineWidth;
|
2024-07-07 23:02:46 +00:00
|
|
|
|
private string mLinePrefix;
|
2024-07-06 21:54:09 +00:00
|
|
|
|
private bool mDebugMode;
|
|
|
|
|
|
2024-07-07 15:48:58 +00:00
|
|
|
|
|
2024-07-06 21:54:09 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Calculates the width of the usable text area, given the current attributes.
|
|
|
|
|
/// </summary>
|
2024-07-07 23:02:46 +00:00
|
|
|
|
private int CalcTextWidth(DataSource source, bool forceFullWidth = false) {
|
|
|
|
|
bool fullWidth = source.FullWidth | forceFullWidth;
|
2024-07-07 15:48:58 +00:00
|
|
|
|
if (source.InBox) {
|
|
|
|
|
if (source.BoxCharIsDefault) {
|
2024-07-07 23:02:46 +00:00
|
|
|
|
// Leave space for left/right box edges, no comment delimiter.
|
|
|
|
|
return mLineWidth - (fullWidth ? 2 : 4);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
} else {
|
|
|
|
|
// Also leave space for a leading comment delimiter, even if the chosen
|
|
|
|
|
// box char happens to match the current delimiter. It might not match when
|
|
|
|
|
// it's rendered for asm gen, and we don't want the output to change.
|
2024-07-07 23:02:46 +00:00
|
|
|
|
return mLineWidth - (fullWidth ? 3 : 5);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return mLineWidth - mLinePrefix.Length;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-04 23:04:33 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Generates one or more lines of formatted text, using the fancy formatter.
|
|
|
|
|
/// </summary>
|
2024-07-07 15:48:58 +00:00
|
|
|
|
/// <param name="formatter">Formatter, which specifies comment delimiters.</param>
|
2024-07-04 23:04:33 +00:00
|
|
|
|
/// <returns>List of formatted strings.</returns>
|
|
|
|
|
private List<string> FormatFancyText(Asm65.Formatter formatter) {
|
2024-07-06 21:54:09 +00:00
|
|
|
|
Debug.Assert(SPACES.Length == MAX_WIDTH);
|
|
|
|
|
|
2024-07-07 15:48:58 +00:00
|
|
|
|
mLineWidth = DEFAULT_WIDTH; // could make this a setting
|
2024-07-06 21:54:09 +00:00
|
|
|
|
mDebugMode = formatter.DebugLongComments;
|
|
|
|
|
mSourceStack.Clear();
|
|
|
|
|
|
|
|
|
|
mLinePrefix = formatter.FullLineCommentDelimiterPlus; // does not change
|
2024-07-07 23:02:46 +00:00
|
|
|
|
//mBoxPrefix = "! "; // changes with [box]
|
2024-07-06 21:54:09 +00:00
|
|
|
|
|
|
|
|
|
DataSource source = new DataSource(Text, 0, null);
|
|
|
|
|
int textWidth = CalcTextWidth(source);
|
2024-07-07 15:48:58 +00:00
|
|
|
|
bool escapeNext = false;
|
|
|
|
|
//bool eatNextIfNewline = false;
|
2024-07-06 21:54:09 +00:00
|
|
|
|
|
|
|
|
|
char[] outBuf = new char[MAX_WIDTH];
|
|
|
|
|
int outIndex = 0;
|
|
|
|
|
int outBreakIndex = -1;
|
|
|
|
|
|
2024-07-04 23:04:33 +00:00
|
|
|
|
List<string> lines = new List<string>();
|
2024-07-06 21:54:09 +00:00
|
|
|
|
if (mDebugMode) {
|
|
|
|
|
for (int i = 0; i < mLineWidth; i++) {
|
|
|
|
|
outBuf[i] = (char)('0' + i % 10);
|
|
|
|
|
}
|
|
|
|
|
lines.Add(new string(outBuf, 0, mLineWidth));
|
2024-07-04 23:04:33 +00:00
|
|
|
|
}
|
2024-07-06 21:54:09 +00:00
|
|
|
|
|
|
|
|
|
// Walk through the input source.
|
|
|
|
|
while (true) {
|
|
|
|
|
if (source.Posn == source.Length) {
|
2024-07-07 15:48:58 +00:00
|
|
|
|
if (mSourceStack.Count == 0) {
|
|
|
|
|
break; // all done
|
2024-07-06 21:54:09 +00:00
|
|
|
|
}
|
2024-07-07 15:48:58 +00:00
|
|
|
|
|
|
|
|
|
source = mSourceStack.Pop();
|
|
|
|
|
textWidth = CalcTextWidth(source);
|
|
|
|
|
continue; // resume earlier string
|
2024-07-06 21:54:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (source.CurChar == '\r' || source.CurChar == '\n') {
|
|
|
|
|
// Explicit line break. If it's a CRLF, eat both.
|
|
|
|
|
if (source.CurChar == '\r' && source.Posn + 1 < source.Length &&
|
|
|
|
|
source[source.Posn + 1] == '\n') {
|
|
|
|
|
source.Posn++;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-07 15:48:58 +00:00
|
|
|
|
escapeNext = false; // can't escape newlines
|
2024-07-06 21:54:09 +00:00
|
|
|
|
|
2024-07-07 15:48:58 +00:00
|
|
|
|
// Output what we have.
|
|
|
|
|
OutputLine(outBuf, outIndex, source, lines);
|
|
|
|
|
outIndex = 0;
|
|
|
|
|
outBreakIndex = -1;
|
2024-07-06 21:54:09 +00:00
|
|
|
|
source.Posn++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char thisCh = source.CurChar;
|
|
|
|
|
if (thisCh == '\\') {
|
2024-07-07 15:48:58 +00:00
|
|
|
|
if (!escapeNext) {
|
|
|
|
|
escapeNext = true;
|
2024-07-06 21:54:09 +00:00
|
|
|
|
source.Posn++; // eat the backslash
|
2024-07-07 15:48:58 +00:00
|
|
|
|
continue; // restart loop to get next char (if any)
|
2024-07-06 21:54:09 +00:00
|
|
|
|
}
|
2024-07-07 15:48:58 +00:00
|
|
|
|
} else if (thisCh == '[' && !escapeNext) {
|
2024-07-06 21:54:09 +00:00
|
|
|
|
// Start of format tag?
|
2024-07-07 23:02:46 +00:00
|
|
|
|
if (TryParseTag(source, formatter, out int skipLen, out DataSource subSource,
|
2024-07-07 15:48:58 +00:00
|
|
|
|
out bool requireLineStart)) {
|
|
|
|
|
if (requireLineStart && outIndex != 0) {
|
|
|
|
|
OutputLine(outBuf, outIndex, source, lines);
|
|
|
|
|
outIndex = 0;
|
|
|
|
|
outBreakIndex = -1;
|
|
|
|
|
}
|
2024-07-06 21:54:09 +00:00
|
|
|
|
source.Posn += skipLen;
|
|
|
|
|
if (subSource != null) {
|
|
|
|
|
mSourceStack.Push(source);
|
|
|
|
|
source = subSource;
|
|
|
|
|
}
|
2024-07-07 15:48:58 +00:00
|
|
|
|
textWidth = CalcTextWidth(source);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
} else if (thisCh == ' ') {
|
|
|
|
|
// Remember position of space for line break. If there are multiple
|
|
|
|
|
// consecutive spaces, remember the position of the first one.
|
|
|
|
|
if (outBreakIndex < 0 || outBuf[outBreakIndex] != ' ' ||
|
|
|
|
|
(outIndex > 0 && outBuf[outIndex - 1] != ' ')) {
|
|
|
|
|
outBreakIndex = outIndex;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-07 15:48:58 +00:00
|
|
|
|
escapeNext = false;
|
2024-07-06 21:54:09 +00:00
|
|
|
|
|
2024-07-07 15:48:58 +00:00
|
|
|
|
// We need to add a character to the out buffer. Will this put us over the limit?
|
2024-07-06 21:54:09 +00:00
|
|
|
|
if (outIndex == textWidth) {
|
|
|
|
|
int outputCount;
|
2024-07-07 23:02:46 +00:00
|
|
|
|
int adj = 0;
|
2024-07-06 21:54:09 +00:00
|
|
|
|
if (outBreakIndex <= 0) {
|
|
|
|
|
// No break found, or break char was at start of line. Just chop what
|
|
|
|
|
// we have.
|
|
|
|
|
outputCount = outIndex;
|
2024-07-07 15:48:58 +00:00
|
|
|
|
if (outputCount > 0 && char.IsSurrogate(outBuf[outIndex - 1])) {
|
|
|
|
|
outputCount--; // don't split surrogate pairs
|
|
|
|
|
}
|
2024-07-06 21:54:09 +00:00
|
|
|
|
} else {
|
|
|
|
|
// Break was a hyphen or space.
|
|
|
|
|
outputCount = outBreakIndex;
|
|
|
|
|
|
2024-07-07 23:02:46 +00:00
|
|
|
|
if (outBuf[outputCount] == '-') {
|
|
|
|
|
// Break was a hyphen, include it.
|
|
|
|
|
adj = 1;
|
|
|
|
|
}
|
2024-07-06 21:54:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Output everything up to the break point, but not the break char itself
|
|
|
|
|
// unless it's a hyphen.
|
2024-07-07 15:48:58 +00:00
|
|
|
|
OutputLine(outBuf, outputCount + adj, source, lines);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
|
|
|
|
|
// Consume any trailing spaces (which are about to become leading spaces).
|
|
|
|
|
while (outputCount < outIndex && outBuf[outputCount] == ' ') {
|
|
|
|
|
outputCount++;
|
|
|
|
|
}
|
|
|
|
|
// Copy any remaining chars to start of buffer.
|
|
|
|
|
outputCount += adj;
|
|
|
|
|
if (outputCount < outIndex) {
|
|
|
|
|
for (int i = 0; i < outIndex - outputCount; i++) {
|
|
|
|
|
outBuf[i] = outBuf[outputCount + i];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
outIndex -= outputCount;
|
|
|
|
|
outBreakIndex = -1;
|
2024-07-07 23:02:46 +00:00
|
|
|
|
Debug.Assert(outIndex >= 0);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
|
|
|
|
|
// If we're at the start of a line, eat all leading spaces. (This is what
|
|
|
|
|
// the WPF TextEdit dialog does when word-wrapping.)
|
|
|
|
|
if (outIndex == 0) {
|
|
|
|
|
while (source.Posn < source.Length && source.CurChar == ' ') {
|
|
|
|
|
source.Posn++;
|
|
|
|
|
}
|
|
|
|
|
if (source.Posn == source.Length) {
|
|
|
|
|
// Whoops, ran out of input.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-07 15:48:58 +00:00
|
|
|
|
// Fold lines at hyphens. We need to check for it after the "line full" test
|
|
|
|
|
// because we want to retain it at the end of the line.
|
2024-07-06 21:54:09 +00:00
|
|
|
|
if (source.CurChar == '-') {
|
|
|
|
|
outBreakIndex = outIndex;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-07 23:02:46 +00:00
|
|
|
|
Debug.Assert(outIndex >= 0 && outIndex < outBuf.Length);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
outBuf[outIndex++] = source[source.Posn++];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we didn't end with a CRLF, output the last bits.
|
|
|
|
|
if (outIndex > 0) {
|
2024-07-07 15:48:58 +00:00
|
|
|
|
OutputLine(outBuf, outIndex, source, lines);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-04 23:04:33 +00:00
|
|
|
|
return lines;
|
|
|
|
|
}
|
2019-05-02 22:45:40 +00:00
|
|
|
|
|
2024-07-06 21:54:09 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Adds the contents of the output buffer to the line list, prefixing it with comment
|
|
|
|
|
/// delimiters and/or wrapping it in a box.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="outBuf">Output buffer.</param>
|
|
|
|
|
/// <param name="length">Length of data in output buffer.</param>
|
|
|
|
|
/// <param name="inBox">True if we're inside a box.</param>
|
|
|
|
|
/// <param name="lines">Line list to add the line to.</param>
|
2024-07-07 15:48:58 +00:00
|
|
|
|
private void OutputLine(char[] outBuf, int length, DataSource source, List<string> lines) {
|
2024-07-06 21:54:09 +00:00
|
|
|
|
Debug.Assert(length >= 0);
|
|
|
|
|
mLineBuilder.Clear();
|
2024-07-07 15:48:58 +00:00
|
|
|
|
if (source.InBox) {
|
2024-07-07 23:02:46 +00:00
|
|
|
|
// If the box character doesn't match the comment delimiter, output the
|
|
|
|
|
// comment delimiter.
|
|
|
|
|
bool boxMatchesCmt = (mLinePrefix[0] == source.BoxChar);
|
|
|
|
|
if (!boxMatchesCmt) {
|
|
|
|
|
mLineBuilder.Append(mLinePrefix[0]);
|
|
|
|
|
}
|
|
|
|
|
mLineBuilder.Append(source.BoxChar);
|
|
|
|
|
if (!source.FullWidth) {
|
|
|
|
|
mLineBuilder.Append(' '); // inset text, unless we're doing an [hr]
|
|
|
|
|
}
|
2024-07-06 21:54:09 +00:00
|
|
|
|
mLineBuilder.Append(outBuf, 0, length);
|
2024-07-07 23:02:46 +00:00
|
|
|
|
// Fill out the rest of the line with spaces, then add the final char.
|
|
|
|
|
int trailingCount = mLineWidth - mLineBuilder.Length;
|
|
|
|
|
// Line is one char shorter when the box character is specified and it matches
|
|
|
|
|
// the comment. (If the box character isn't specified then it always matches
|
|
|
|
|
// the comment; if the box doesn't match the comment then we're shoved over one
|
|
|
|
|
// char because the comment delimiter is present.)
|
|
|
|
|
if (!source.BoxCharIsDefault && boxMatchesCmt) {
|
|
|
|
|
trailingCount--;
|
|
|
|
|
}
|
|
|
|
|
if (trailingCount > 1) {
|
|
|
|
|
mLineBuilder.Append(SPACES, 0, trailingCount - 1);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
}
|
2024-07-07 23:02:46 +00:00
|
|
|
|
mLineBuilder.Append(source.BoxChar);
|
2024-07-06 21:54:09 +00:00
|
|
|
|
} else {
|
2024-07-07 15:48:58 +00:00
|
|
|
|
if (!source.SuppressPrefix) {
|
|
|
|
|
mLineBuilder.Append(mLinePrefix);
|
|
|
|
|
}
|
2024-07-06 21:54:09 +00:00
|
|
|
|
mLineBuilder.Append(outBuf, 0, length);
|
|
|
|
|
}
|
|
|
|
|
string str = mLineBuilder.ToString();
|
|
|
|
|
if (mDebugMode) {
|
|
|
|
|
str = str.Replace(' ', '\u2219'); // replace spaces with BULLET OPERATOR
|
|
|
|
|
}
|
|
|
|
|
lines.Add(str);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-07 15:48:58 +00:00
|
|
|
|
private enum Tag {
|
2024-07-07 23:02:46 +00:00
|
|
|
|
Unknown = 0, Break, HorizRule, Width, BoxStart, UrlStart
|
2024-07-07 15:48:58 +00:00
|
|
|
|
}
|
|
|
|
|
private class TagMatch {
|
|
|
|
|
public string mPatStr;
|
|
|
|
|
public Tag mTag;
|
|
|
|
|
|
|
|
|
|
public TagMatch(string pat, Tag tag) {
|
|
|
|
|
mPatStr = pat;
|
|
|
|
|
mTag = tag;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private static readonly TagMatch[] sTagTable = {
|
|
|
|
|
new TagMatch("br", Tag.Break),
|
|
|
|
|
new TagMatch("hr", Tag.HorizRule),
|
|
|
|
|
new TagMatch("width", Tag.Width),
|
|
|
|
|
new TagMatch("box", Tag.BoxStart),
|
|
|
|
|
new TagMatch("url", Tag.UrlStart),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Attempts to parse a tag at the current source position.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// <para>This attempts to parse the full tag, including the closing tag if such is
|
|
|
|
|
/// appropriate.</para>
|
|
|
|
|
/// </remarks>
|
|
|
|
|
/// <param name="source">Input data source.</param>
|
2024-07-07 23:02:46 +00:00
|
|
|
|
/// <param name="formatter">Output formatter.</param>
|
2024-07-07 15:48:58 +00:00
|
|
|
|
/// <param name="skipLen">Number of characters to advance in data source.</param>
|
|
|
|
|
/// <param name="subSource">Result: data source with tag contents. May be null.</param>
|
|
|
|
|
/// <param name="requireLineStart">Result: if true, and the output buffer has characters
|
|
|
|
|
/// in it, they must be flushed before continuing.</param>
|
|
|
|
|
/// <returns>True if the tag was successfully parsed.</returns>
|
2024-07-07 23:02:46 +00:00
|
|
|
|
private bool TryParseTag(DataSource source, Asm65.Formatter formatter,
|
|
|
|
|
out int skipLen, out DataSource subSource, out bool requireLineStart) {
|
2024-07-07 15:48:58 +00:00
|
|
|
|
skipLen = 0;
|
|
|
|
|
requireLineStart = false;
|
|
|
|
|
subSource = null;
|
|
|
|
|
|
|
|
|
|
Tag tag = Tag.Unknown;
|
|
|
|
|
foreach (TagMatch pat in sTagTable) {
|
|
|
|
|
if (source.Match(pat.mPatStr, 1)) {
|
|
|
|
|
tag = pat.mTag;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
string tagStr = null;
|
|
|
|
|
if (tag != Tag.Unknown) {
|
|
|
|
|
// Look for the end.
|
|
|
|
|
for (int endpos = source.Posn + 2; endpos < source.Length; endpos++) {
|
2024-07-07 23:02:46 +00:00
|
|
|
|
char ch = source[endpos];
|
|
|
|
|
if (ch == ']') {
|
2024-07-07 15:48:58 +00:00
|
|
|
|
// Found the end of the tag.
|
|
|
|
|
tagStr = source.Text.Substring(source.Posn, endpos - source.Posn + 1);
|
|
|
|
|
break;
|
2024-07-07 23:02:46 +00:00
|
|
|
|
} else if (ch == '\r' || ch == '\n') {
|
|
|
|
|
// Stop looking if we hit a line break mid-tag.
|
|
|
|
|
break;
|
2024-07-07 15:48:58 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (tagStr == null) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2024-07-07 23:02:46 +00:00
|
|
|
|
//Debug.WriteLine("Initial match at " + source.Posn + ": " + tag + " '" + tagStr + "'");
|
2024-07-07 15:48:58 +00:00
|
|
|
|
|
|
|
|
|
bool eatNextIfNewline = false;
|
|
|
|
|
switch (tag) {
|
|
|
|
|
case Tag.Break:
|
|
|
|
|
int brWidth = "[br]".Length;
|
|
|
|
|
if (tagStr.Length != brWidth) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2024-07-07 23:02:46 +00:00
|
|
|
|
if (source.InsideElement) {
|
|
|
|
|
return false; // can't use inside a box
|
2024-07-07 15:48:58 +00:00
|
|
|
|
}
|
|
|
|
|
skipLen = brWidth;
|
|
|
|
|
// Just a blank line, but with "suppress prefix" enabled.
|
|
|
|
|
requireLineStart = eatNextIfNewline = true;
|
|
|
|
|
subSource = new DataSource("\r\n", 0, source);
|
|
|
|
|
subSource.SuppressPrefix = true;
|
|
|
|
|
break;
|
|
|
|
|
case Tag.HorizRule:
|
|
|
|
|
if (source.InsideNonBoxElement) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
char defaultCh;
|
|
|
|
|
if (source.InBox) {
|
|
|
|
|
defaultCh = source.BoxChar;
|
|
|
|
|
} else {
|
|
|
|
|
defaultCh = DEFAULT_RULE_CHAR;
|
|
|
|
|
}
|
2024-07-07 23:02:46 +00:00
|
|
|
|
if (!HandleHorizRule(tagStr, defaultCh, out skipLen, out char hrChar)) {
|
2024-07-07 15:48:58 +00:00
|
|
|
|
return false;
|
|
|
|
|
}
|
2024-07-07 23:02:46 +00:00
|
|
|
|
int ruleWidth = CalcTextWidth(source, true);
|
2024-07-07 15:48:58 +00:00
|
|
|
|
StringBuilder rulerSb = new StringBuilder(ruleWidth);
|
|
|
|
|
for (int i = 0; i < ruleWidth; i++) {
|
|
|
|
|
rulerSb.Append(hrChar);
|
|
|
|
|
}
|
2024-07-07 23:02:46 +00:00
|
|
|
|
rulerSb.Append("\r\n");
|
2024-07-07 15:48:58 +00:00
|
|
|
|
subSource = new DataSource(rulerSb.ToString(), 0, source);
|
2024-07-07 23:02:46 +00:00
|
|
|
|
subSource.FullWidth = true;
|
2024-07-07 15:48:58 +00:00
|
|
|
|
requireLineStart = eatNextIfNewline = true;
|
|
|
|
|
break;
|
|
|
|
|
case Tag.Width:
|
2024-07-07 23:02:46 +00:00
|
|
|
|
if (source.InsideElement) {
|
2024-07-07 15:48:58 +00:00
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
int newWidth = HandleWidth(tagStr, out skipLen);
|
|
|
|
|
if (newWidth < 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
requireLineStart = eatNextIfNewline = true;
|
|
|
|
|
mLineWidth = newWidth;
|
|
|
|
|
break;
|
2024-07-07 23:02:46 +00:00
|
|
|
|
case Tag.BoxStart:
|
|
|
|
|
if (source.InsideElement) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
char defBoxChar = formatter.FullLineCommentDelimiterBase[0];
|
|
|
|
|
if (!HandleBox(tagStr, source, defBoxChar, out skipLen, out char boxChar,
|
|
|
|
|
out bool isBoxCharDef, out string insideBox)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
requireLineStart = eatNextIfNewline = true;
|
|
|
|
|
subSource = new DataSource(insideBox, 0, source);
|
|
|
|
|
subSource.InBox = true;
|
|
|
|
|
subSource.BoxChar = boxChar;
|
|
|
|
|
subSource.BoxCharIsDefault = isBoxCharDef;
|
|
|
|
|
break;
|
|
|
|
|
case Tag.UrlStart:
|
|
|
|
|
if (source.InsideNonBoxElement) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!HandleUrl(tagStr, source, out skipLen, out string showText)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
requireLineStart = eatNextIfNewline = false;
|
|
|
|
|
subSource = new DataSource(showText, 0, source);
|
|
|
|
|
subSource.InUrl = true;
|
|
|
|
|
break;
|
2024-07-07 15:48:58 +00:00
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Some tags cause a newline to happen, e.g. [box] and [hr] always start on a new line
|
|
|
|
|
// of output. It can feel natural to type these on a line by themselves, but that
|
|
|
|
|
// will generate an extra newline unless we suppress it here.
|
|
|
|
|
if (eatNextIfNewline) {
|
|
|
|
|
if (source.Posn + skipLen < source.Length &&
|
|
|
|
|
source[source.Posn + skipLen] == '\r') {
|
|
|
|
|
skipLen++;
|
|
|
|
|
}
|
|
|
|
|
if (source.Posn + skipLen < source.Length &&
|
|
|
|
|
source[source.Posn + skipLen] == '\n') {
|
|
|
|
|
skipLen++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Parses an [hr] or [hr char='x'] tag. Returns the ruler char, or '\0' on error.
|
|
|
|
|
/// </summary>
|
2024-07-07 23:02:46 +00:00
|
|
|
|
private static bool HandleHorizRule(string tagStr, char defaultChar, out int skipLen,
|
|
|
|
|
out char hrChar) {
|
2024-07-07 15:48:58 +00:00
|
|
|
|
const string simpleForm = "[hr]";
|
|
|
|
|
const string prefix = "[hr char='";
|
|
|
|
|
const string suffix = "']";
|
2024-07-07 23:02:46 +00:00
|
|
|
|
hrChar = '\0';
|
2024-07-07 15:48:58 +00:00
|
|
|
|
skipLen = tagStr.Length;
|
|
|
|
|
|
|
|
|
|
if (tagStr.Equals(simpleForm, StringComparison.OrdinalIgnoreCase)) {
|
|
|
|
|
// use default char
|
|
|
|
|
hrChar = defaultChar;
|
|
|
|
|
} else if (tagStr.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) {
|
|
|
|
|
// char explicitly set
|
|
|
|
|
int charStrLen = tagStr.Length - prefix.Length - suffix.Length;
|
|
|
|
|
if (charStrLen != 1) {
|
2024-07-07 23:02:46 +00:00
|
|
|
|
return false;
|
2024-07-07 15:48:58 +00:00
|
|
|
|
}
|
|
|
|
|
if (!tagStr.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase)) {
|
2024-07-07 23:02:46 +00:00
|
|
|
|
return false;
|
2024-07-07 15:48:58 +00:00
|
|
|
|
}
|
|
|
|
|
hrChar = tagStr[prefix.Length];
|
|
|
|
|
} else {
|
2024-07-07 23:02:46 +00:00
|
|
|
|
return false;
|
2024-07-07 15:48:58 +00:00
|
|
|
|
}
|
2024-07-07 23:02:46 +00:00
|
|
|
|
return true;
|
2024-07-07 15:48:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Parses a [width] tag. Returns the width, or -1 on error.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static int HandleWidth(string tagStr, out int skipLen) {
|
|
|
|
|
const string prefix = "[width=";
|
|
|
|
|
const string suffix = "]";
|
|
|
|
|
skipLen = 0;
|
|
|
|
|
|
|
|
|
|
int widthStrLen = tagStr.Length - prefix.Length - suffix.Length;
|
|
|
|
|
if (widthStrLen <= 0) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
if (!tagStr.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
if (!tagStr.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase)) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
string widthStr = tagStr.Substring(prefix.Length, widthStrLen);
|
|
|
|
|
int newWidth;
|
|
|
|
|
if (widthStr == "*") {
|
|
|
|
|
newWidth = DEFAULT_WIDTH;
|
|
|
|
|
} else if (!int.TryParse(widthStr, out newWidth)) {
|
|
|
|
|
Debug.WriteLine("Unable to parse width '" + widthStr + "'");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
if (newWidth < MIN_WIDTH || newWidth > MAX_WIDTH) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
skipLen = tagStr.Length;
|
|
|
|
|
return newWidth;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-07 23:02:46 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Parses a [box]...[/box] tag, which could also be [box char='x'].
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static bool HandleBox(string tagStr, DataSource source, char defBoxChar,
|
|
|
|
|
out int skipLen, out char boxChar, out bool isBoxCharDef, out string insideBox) {
|
|
|
|
|
const string startTagDefault = "[box]";
|
|
|
|
|
const string startTagPrefix = "[box char='";
|
|
|
|
|
const string startTagSuffix = "']";
|
|
|
|
|
const string endTag = "[/box]";
|
|
|
|
|
skipLen = 0;
|
|
|
|
|
boxChar = '?';
|
|
|
|
|
isBoxCharDef = false;
|
|
|
|
|
insideBox = "!!!";
|
|
|
|
|
|
|
|
|
|
if (tagStr.Equals(startTagDefault, StringComparison.InvariantCultureIgnoreCase)) {
|
|
|
|
|
boxChar = defBoxChar;
|
|
|
|
|
isBoxCharDef = true;
|
|
|
|
|
} else if (tagStr.StartsWith(startTagPrefix,
|
|
|
|
|
StringComparison.InvariantCultureIgnoreCase) &&
|
|
|
|
|
tagStr.EndsWith(startTagSuffix,
|
|
|
|
|
StringComparison.InvariantCultureIgnoreCase) &&
|
|
|
|
|
tagStr.Length == startTagPrefix.Length + 1 + startTagSuffix.Length) {
|
|
|
|
|
boxChar = tagStr[startTagPrefix.Length];
|
|
|
|
|
isBoxCharDef = false;
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int boxEndPosn = source.FindNext(endTag);
|
|
|
|
|
if (boxEndPosn < 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int innerLen = boxEndPosn - (source.Posn + tagStr.Length);
|
|
|
|
|
skipLen = tagStr.Length + innerLen + endTag.Length;
|
|
|
|
|
insideBox = "[hr]" + source.Text.Substring(source.Posn + tagStr.Length, innerLen) +
|
|
|
|
|
"[hr]";
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Parses a [url]...[/url] tag, which could also be [url=xyzzy].
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static bool HandleUrl(string tagStr, DataSource source,
|
|
|
|
|
out int skipLen, out string showText) {
|
|
|
|
|
const string simpleStart = "[url]";
|
|
|
|
|
const string linkStartPrefix = "[url=";
|
|
|
|
|
const string linkStartSuffix = "]";
|
|
|
|
|
const string endTag = "[/url]";
|
|
|
|
|
skipLen = 0;
|
|
|
|
|
showText = string.Empty;
|
|
|
|
|
|
|
|
|
|
string linkStr;
|
|
|
|
|
if (tagStr.Equals(simpleStart, StringComparison.InvariantCultureIgnoreCase)) {
|
|
|
|
|
// The text is also the link.
|
|
|
|
|
linkStr = string.Empty;
|
|
|
|
|
} else if (tagStr.StartsWith(linkStartPrefix,
|
|
|
|
|
StringComparison.InvariantCultureIgnoreCase) &&
|
|
|
|
|
tagStr.EndsWith(linkStartSuffix,
|
|
|
|
|
StringComparison.InvariantCultureIgnoreCase) &&
|
|
|
|
|
tagStr.Length > linkStartPrefix.Length + linkStartSuffix.Length) {
|
|
|
|
|
// URI is specified in tag.
|
|
|
|
|
linkStr = tagStr.Substring(linkStartPrefix.Length,
|
|
|
|
|
tagStr.Length - (linkStartPrefix.Length + linkStartSuffix.Length));
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int urlEndPosn = source.FindNext(endTag);
|
|
|
|
|
if (urlEndPosn < 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int innerLen = urlEndPosn - (source.Posn + tagStr.Length);
|
|
|
|
|
skipLen = tagStr.Length + innerLen + endTag.Length;
|
|
|
|
|
showText = source.Text.Substring(source.Posn + tagStr.Length, innerLen);
|
|
|
|
|
if (!string.IsNullOrEmpty(linkStr)) {
|
|
|
|
|
showText += " (" + linkStr + ")";
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-06 21:54:09 +00:00
|
|
|
|
#endregion Fancy
|
|
|
|
|
|
|
|
|
|
public override string ToString() {
|
|
|
|
|
if (IsFancy) {
|
|
|
|
|
return "MLC fancy text='" + Text + "'";
|
|
|
|
|
} else {
|
|
|
|
|
return "MLC box=" + BoxMode + " width=" + MaxWidth + " text='" + Text + "'";
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-05-02 22:45:40 +00:00
|
|
|
|
|
|
|
|
|
public static bool operator ==(MultiLineComment a, MultiLineComment b) {
|
|
|
|
|
if (ReferenceEquals(a, b)) {
|
|
|
|
|
return true; // same object, or both null
|
|
|
|
|
}
|
|
|
|
|
if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) {
|
|
|
|
|
return false; // one is null
|
|
|
|
|
}
|
|
|
|
|
return a.Text.Equals(b.Text) && a.BoxMode == b.BoxMode && a.MaxWidth == b.MaxWidth
|
2024-07-20 20:05:50 +00:00
|
|
|
|
&& a.BackgroundColor == b.BackgroundColor && a.IsFancy == b.IsFancy;
|
2019-05-02 22:45:40 +00:00
|
|
|
|
}
|
|
|
|
|
public static bool operator !=(MultiLineComment a, MultiLineComment b) {
|
|
|
|
|
return !(a == b);
|
|
|
|
|
}
|
|
|
|
|
public override bool Equals(object obj) {
|
|
|
|
|
return obj is MultiLineComment && this == (MultiLineComment)obj;
|
|
|
|
|
}
|
|
|
|
|
public override int GetHashCode() {
|
2024-07-20 20:05:50 +00:00
|
|
|
|
return Text.GetHashCode() ^ MaxWidth ^ (BoxMode ? 1 : 0) ^ (IsFancy ? 2 : 0) ^
|
|
|
|
|
BackgroundColor.GetHashCode();
|
2019-05-02 22:45:40 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|