using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Folding; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace LunarSF.SHomeWorkshop.LunarMarkdownEditor { /// <summary> /// 此类用以定义编辑器的折叠区。 /// </summary> class CustomFoldingStrategy : AbstractFoldingStrategy { private ICSharpCode.AvalonEdit.TextEditor edit; /// <summary> /// 要应用折叠功能的编辑器。 /// </summary> public ICSharpCode.AvalonEdit.TextEditor Edit { get { return this.edit; } } /// <summary> /// [构造方法]创建一个新的折叠区。 /// </summary> public CustomFoldingStrategy(ICSharpCode.AvalonEdit.TextEditor edit) { this.edit = edit; } /// <summary> /// 为特定文档创建 NewFolding 对象集。 /// </summary> public override IEnumerable<NewFolding> CreateNewFoldings(TextDocument document, out int firstErrorOffset) { firstErrorOffset = -1; return CreateNewFoldings(document); } /// <summary> /// 为特定文档创建 NewFolding 对象集。 /// </summary> public IEnumerable<NewFolding> CreateNewFoldings(ITextSource document) { List<NewFolding> newFoldings = new List<NewFolding>(); #region 使六级标题都支持折叠 List<HeaderInfo> preHeadersInfos = new List<HeaderInfo>(); for (int i = 1; i <= edit.LineCount; i++) { var line = edit.Document.GetLineByNumber(i); string text = edit.Document.GetText(line.Offset, line.Length); if (text.StartsWith("#") == false && CustomMarkdownSupport.IsTaskLine(text) == false) continue; var headerText = GetHeaderOfTitleOrTaskListItem(text); var contentText = GetContentOfHeaderOrTaskListItem(text); var newHeaderInfo = new HeaderInfo() { HeaderText = headerText, ContentText = contentText, Length = headerText.Length, Offset = line.Offset, EndOffset = line.EndOffset, }; if (preHeadersInfos.Count <= 0) { preHeadersInfos.Add(newHeaderInfo); continue; } for (int j = preHeadersInfos.Count - 1; j >= 0; j--) { if (newHeaderInfo.Length <= preHeadersInfos[j].Length) { var endOffset = newHeaderInfo.Offset - 2;//-2是\r\n的宽度 //如果在这个标题只有一行,不折叠 if (endOffset > preHeadersInfos[j].EndOffset) { var newFolding = new NewFolding(preHeadersInfos[j].Offset, newHeaderInfo.Offset - 2) { Name = BuildHeaderOrTaskListItemHeader(preHeadersInfos[j].HeaderText) + (string.IsNullOrEmpty(preHeadersInfos[j].ContentText) ? "... " : preHeadersInfos[j].ContentText) + " ※ ", }; newFoldings.Add(newFolding); } preHeadersInfos.RemoveAt(j); } } preHeadersInfos.Add(newHeaderInfo); } //到最后一行,看看要不要添加折叠块。 if (preHeadersInfos.Count > 0) { var lastLine = this.edit.Document.GetLineByNumber(this.edit.Document.LineCount); if (lastLine != null) { var lastLineText = this.edit.Document.GetText(lastLine); if (lastLineText.StartsWith("#") == false && CustomMarkdownSupport.IsTaskLine(lastLineText) == false) { for (int j = preHeadersInfos.Count - 1; j >= 0; j--) { var endOffset = edit.Document.Lines.Last().Offset;//最后一行,没有\r\n,不用 - 2; //如果在这个标题只有一行,不折叠 if (endOffset > preHeadersInfos[j].EndOffset) { newFoldings.Add(new NewFolding(preHeadersInfos[j].Offset, endOffset) { Name = BuildHeaderOrTaskListItemHeader(preHeadersInfos[j].HeaderText) + (string.IsNullOrEmpty(preHeadersInfos[j].ContentText) ? "... " : preHeadersInfos[j].ContentText + " ※ "), }); } preHeadersInfos.RemoveAt(j); } } } } #endregion #region 添加试题所需要的折叠块 Stack<int> startOffsets = new Stack<int>(); string openingMark = "试题>>"; string closingMark = "〓〓〓〓〓〓"; for (int i = 1; i <= edit.LineCount; i++) { var line = edit.Document.GetLineByNumber(i); var preLine = line.PreviousLine; string text = edit.Document.GetText(line.Offset, line.Length).TrimStart(); if (text.StartsWith(openingMark)) { if (preLine != null && startOffsets.Count > 0) { int startOffset = startOffsets.Pop(); // 首尾标记在同一行时不折叠。 if (startOffset < preLine.Offset) { newFoldings.Add(new NewFolding(startOffset, preLine.EndOffset) { Name = openingMark, IsSpecial = true, }); } } startOffsets.Push(line.Offset); } else if (text.StartsWith(closingMark) && startOffsets.Count > 0) { int startOffset = startOffsets.Pop(); // 首尾标记在同一行时不折叠。 if (startOffset < line.EndOffset) { newFoldings.Add(new NewFolding(startOffset, line.EndOffset) { Name = openingMark, IsSpecial = true, }); } } } #endregion #region 使二维文字表支持折叠 int fstTextTableLineStartOffset = -1; int fstTextTableLineEndOffset = -1; int preTextTableLineStartOffset = -1; int preTextTableLineEndOffset = -1; int currentLineEndOffset = -1; string tableTitle = ""; for (int i = 1; i <= edit.LineCount; i++) { var line = edit.Document.GetLineByNumber(i); currentLineEndOffset = line.EndOffset; string lineText = edit.Document.GetText(line.Offset, line.Length); if (lineText.IndexOf('|') >= 0 || lineText.IndexOf('|') >= 0) { if (fstTextTableLineStartOffset < 0) { preTextTableLineStartOffset = fstTextTableLineStartOffset = line.Offset; preTextTableLineEndOffset = fstTextTableLineEndOffset = line.EndOffset; string[] pieces = lineText.Split(new char[] { '|', '|' }, StringSplitOptions.RemoveEmptyEntries); foreach (var piece in pieces) { var text = piece.Trim(new char[] { ' ', ' ', '\t' }); if (string.IsNullOrWhiteSpace(text) == false) { tableTitle = text; break; } } } else { preTextTableLineStartOffset = line.Offset; preTextTableLineEndOffset = line.EndOffset; } } else { if (CustomMarkdownSupport.IsCommentLine(lineText)) { preTextTableLineStartOffset = line.Offset; preTextTableLineEndOffset = line.EndOffset; continue; } else { if (fstTextTableLineStartOffset >= 0 && preTextTableLineEndOffset >= 0) { var ttfolding = new NewFolding(fstTextTableLineStartOffset, preTextTableLineEndOffset)//不该把同级行折叠起来 { Name = $"┣{(string.IsNullOrWhiteSpace(tableTitle) ? "文字表" : tableTitle)}┫", IsSpecial = true, }; newFoldings.Add(ttfolding); //还原 fstTextTableLineEndOffset = -1; fstTextTableLineStartOffset = -1; preTextTableLineEndOffset = -1; preTextTableLineStartOffset = -1; } } } } //防止最后一行也是二维文字表行。 if (fstTextTableLineStartOffset >= 0 && preTextTableLineEndOffset >= 0) { var ttfolding = new NewFolding(fstTextTableLineStartOffset, (currentLineEndOffset >= 0 ? currentLineEndOffset : preTextTableLineEndOffset))//不该把同级行折叠起来 { Name = $"二维文字表{(string.IsNullOrWhiteSpace(tableTitle) ? "" : ":")}{tableTitle}", IsSpecial = true, }; newFoldings.Add(ttfolding); //还原 fstTextTableLineEndOffset = -1; fstTextTableLineStartOffset = -1; preTextTableLineEndOffset = -1; preTextTableLineStartOffset = -1; } #endregion #region 使引用块支持折叠 int fstBlockQuoteLineStartOffset = -1; int fstBlockQuoteLineEndOffset = -1; int preBlockQuoteLineStartOffset = -1; int preBlockQuoteLineEndOffset = -1; int currentBlockQuoteLineEndOffset = -1; string blockQuoteTitle = ""; for (int i = 1; i <= edit.LineCount; i++) { var line = edit.Document.GetLineByNumber(i); currentBlockQuoteLineEndOffset = line.EndOffset; string lineText = edit.Document.GetText(line.Offset, line.Length); if (CustomMarkdownSupport.IsBlockQuoteLine(lineText)) { if (fstBlockQuoteLineStartOffset < 0) { preBlockQuoteLineStartOffset = fstBlockQuoteLineStartOffset = line.Offset; preBlockQuoteLineEndOffset = fstBlockQuoteLineEndOffset = line.EndOffset; var text = lineText.Trim(new char[] { ' ', ' ', '\t' }); Regex regexBlockQuoter = new Regex(@"^[ ]{0,3}([>》〉]{1}[ ]{0,}){1,}"); var matchBlockQuoter = regexBlockQuoter.Match(text); if (matchBlockQuoter != null && matchBlockQuoter.Success) { text = text.Substring(matchBlockQuoter.Length); } if (string.IsNullOrWhiteSpace(blockQuoteTitle) && string.IsNullOrWhiteSpace(text) == false)//保留第一行作标题。 { blockQuoteTitle = text; if (blockQuoteTitle.Length > 36) { blockQuoteTitle = blockQuoteTitle.Substring(0, 36) + "..."; } } } else { preBlockQuoteLineStartOffset = line.Offset; preBlockQuoteLineEndOffset = line.EndOffset; } } else { if (fstBlockQuoteLineStartOffset >= 0 && preBlockQuoteLineEndOffset >= 0) { var bqfolding = new NewFolding(fstBlockQuoteLineStartOffset, preBlockQuoteLineEndOffset)//不该把同级行折叠起来 { Name = $"> {(string.IsNullOrWhiteSpace(blockQuoteTitle) ? "引用块" : blockQuoteTitle)}", IsSpecial = false, }; newFoldings.Add(bqfolding); //还原 fstBlockQuoteLineEndOffset = -1; fstBlockQuoteLineStartOffset = -1; preBlockQuoteLineEndOffset = -1; preBlockQuoteLineStartOffset = -1; blockQuoteTitle = null; } } } //防止最后一行也是引用块行。 if (fstBlockQuoteLineStartOffset >= 0 && preBlockQuoteLineEndOffset >= 0) { var bqfolding = new NewFolding(fstBlockQuoteLineStartOffset, (currentBlockQuoteLineEndOffset >= 0 ? currentBlockQuoteLineEndOffset : preBlockQuoteLineEndOffset))//不该把同级行折叠起来 { Name = $"引用块{(string.IsNullOrWhiteSpace(blockQuoteTitle) ? "" : ":")}{blockQuoteTitle}", IsSpecial = false, }; newFoldings.Add(bqfolding); //还原 fstBlockQuoteLineEndOffset = -1; fstBlockQuoteLineStartOffset = -1; preBlockQuoteLineEndOffset = -1; preBlockQuoteLineStartOffset = -1; blockQuoteTitle = null; } #endregion #region 使树型文字表支持折叠 List<TreeListTextLine> foldTreeLines = null; List<List<TreeListTextLine>> listss = new List<List<TreeListTextLine>>(); //双层列表嵌套,这是为了解决多个不连续的树型文字表 for (int i = 1; i <= edit.LineCount; i++) { var line = edit.Document.GetLineByNumber(i); string lineText = edit.Document.GetText(line.Offset, line.Length); string header, tail; int level; if (MarkDownEditorBase.IsTreeListLine(lineText, out header, out tail, out level)) { var ti = new TreeListTextLine() { Line = line, HeaderTextArray = header.ToCharArray(), OldLevel = level, NewLevel = level, OldText = lineText, NewText = lineText, TailText = tail, }; if (foldTreeLines == null) { foldTreeLines = new List<TreeListTextLine>(); listss.Add(foldTreeLines); } foldTreeLines.Add(ti); } else { if (foldTreeLines != null) foldTreeLines = null; } } foreach (List<TreeListTextLine> list in listss) { if (list.Count <= 1) continue; for (int j = 0; j < list.Count - 1; j++)//最后一个没必要折叠 { TreeListTextLine tltl = list[j]; NewFolding ttfolding = null; for (int k = j + 1; k < list.Count; k++)//但最后一个有必要判断是否完成折叠 { var nexttltl = list[k]; if (nexttltl.NewLevel <= tltl.NewLevel) { //这个对象并非无意义,但不能放到下一层判断之中。 ttfolding = new NewFolding(tltl.Line.Offset, nexttltl.Line.Offset - 2)//不该把同级行折叠起来 { Name = tltl.NewText, IsSpecial = true, }; if (nexttltl.Line.LineNumber > tltl.Line.LineNumber + 1)//如果只有一行,不加折叠 { newFoldings.Add(ttfolding); } break; } } if (ttfolding == null && tltl.Line.LineNumber < list[list.Count - 1].Line.LineNumber) { ttfolding = new NewFolding(tltl.Line.Offset, list[list.Count - 1].Line.EndOffset) { Name = tltl.NewText, IsSpecial = true, }; newFoldings.Add(ttfolding); } } } #endregion #region 用户自定义 JavaScript 代码块 string startRegionScript = @"^[<][Ss][Cc][Rr][Ii][Pp][Tt]([ ]{1,}.*)?[>]"; string endRegionScript = @"[<][/][Ss][Cc][Rr][Ii][Pp][Tt][>]"; //短的一行就可以。 Stack<LineInfo> regionStartLinesStackScript = new Stack<LineInfo>(); for (int i = 1; i <= edit.LineCount; i++) { var lineScript = edit.Document.GetLineByNumber(i); string lineTextScript = edit.Document.GetText(lineScript.Offset, lineScript.Length); Regex regexStartScript = new Regex(startRegionScript); var matchStartScript = regexStartScript.Match(lineTextScript); if (matchStartScript != null && matchStartScript.Success) { regionStartLinesStackScript.Push(new LunarMarkdownEditor.LineInfo() { Line = lineScript, LineText = lineTextScript, HeaderText = lineTextScript.Substring(matchStartScript.Length), }); } else { Regex regexEndScript = new Regex(endRegionScript); var matchEndScript = regexEndScript.Match(lineTextScript); if (matchEndScript != null && matchEndScript.Success)// && preRegionLine != null) { if (regionStartLinesStackScript.Count > 0) { var preRegionLineScript = regionStartLinesStackScript.Pop(); newFoldings.Add(new NewFolding(preRegionLineScript.Line.Offset, lineScript.EndOffset) { Name = "<Script />", IsSpecial = true, }); } } } } #endregion #region 用户自定义内部样式表区域 string startRegionStyle = @"^[<][Ss][Tt][Yy][Ll][Ee]([ ]{1,}.*)?[>]"; string endRegionStyle = @"[<][/][Ss][Tt][Yy][Ll][Ee][>]"; //短的一行就可以。 Stack<LineInfo> regionStartLinesStackStyle = new Stack<LineInfo>(); for (int i = 1; i <= edit.LineCount; i++) { var lineStyle = edit.Document.GetLineByNumber(i); string lineTextStyle = edit.Document.GetText(lineStyle.Offset, lineStyle.Length); Regex regexStartStyle = new Regex(startRegionStyle); var matchStartStyle = regexStartStyle.Match(lineTextStyle); if (matchStartStyle != null && matchStartStyle.Success) { regionStartLinesStackStyle.Push(new LunarMarkdownEditor.LineInfo() { Line = lineStyle, LineText = lineTextStyle, HeaderText = lineTextStyle.Substring(matchStartStyle.Length), }); } else { Regex regexEndStyle = new Regex(endRegionStyle); var matchEndStyle = regexEndStyle.Match(lineTextStyle); if (matchEndStyle != null && matchEndStyle.Success)// && preRegionLine != null) { if (regionStartLinesStackStyle.Count > 0) { var preRegionLineStyle = regionStartLinesStackStyle.Pop(); newFoldings.Add(new NewFolding(preRegionLineStyle.Line.Offset, lineStyle.EndOffset) { Name = "<Style />", IsSpecial = true, }); } } } } #endregion #region 添加方块文本 string startRegionS = @"^[ ]{0,3}(\[[ \t]*(?!(.*\].*)))"; string endRegionS = @"^[ ]{0,3}\].*"; Stack<LineInfo> regionStartLinesStackS = new Stack<LineInfo>(); for (int i = 1; i <= edit.LineCount; i++) { var lineS = edit.Document.GetLineByNumber(i); string lineTextS = edit.Document.GetText(lineS.Offset, lineS.Length); Regex regexStartS = new Regex(startRegionS); var matchStartS = regexStartS.Match(lineTextS); if (matchStartS != null && matchStartS.Success) { regionStartLinesStackS.Push(new LunarMarkdownEditor.LineInfo() { Line = lineS, LineText = lineTextS, HeaderText = lineTextS.Substring(matchStartS.Length), }); } else { Regex regexEndS = new Regex(endRegionS); var matchEndS = regexEndS.Match(lineTextS); if (matchEndS != null && matchEndS.Success)// && preRegionLine != null) { if (regionStartLinesStackS.Count > 0) { var preRegionLineS = regionStartLinesStackS.Pop(); newFoldings.Add(new NewFolding(preRegionLineS.Line.Offset, lineS.EndOffset) { Name = (preRegionLineS.HeaderText == null ? $"[{lineTextS.Substring(matchEndS.Length)}]" : $"[{preRegionLineS.HeaderText}]"), IsSpecial = true, }); } } } } #endregion #region 添加自定义折叠块 string startRegion = @"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]*)?([!?!?IWEQIWEQiweqiweq][ \t]*)?\{[ \t]*"; string endRegion = @"^[ ]{0,3}\}[ \t]*([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]*)?"; Stack<LineInfo> regionStartLinesStack = new Stack<LineInfo>(); for (int i = 1; i <= edit.LineCount; i++) { var line = edit.Document.GetLineByNumber(i); string lineText = edit.Document.GetText(line.Offset, line.Length); Regex regexStart = new Regex(startRegion); var matchStart = regexStart.Match(lineText); if (matchStart != null && matchStart.Success) { regionStartLinesStack.Push(new LunarMarkdownEditor.LineInfo() { Line = line, LineText = lineText, HeaderText = lineText.Substring(matchStart.Length), }); } else { Regex regexEnd = new Regex(endRegion); var matchEnd = regexEnd.Match(lineText); if (matchEnd != null && matchEnd.Success)// && preRegionLine != null) { if (regionStartLinesStack.Count > 0) { var preRegionLine = regionStartLinesStack.Pop(); newFoldings.Add(new NewFolding(preRegionLine.Line.Offset, line.EndOffset) { Name = (preRegionLine.HeaderText == null ? $"{{{lineText.Substring(matchEnd.Length)}}}" : $"{{{preRegionLine.HeaderText}}}"), IsSpecial = true, }); } } } } #endregion #region 单行图像链接折叠或文件链接 Regex regImageOrFileHeader = new Regex(@"(?<=(!?\[)).*(?=\])"); for (int i = 1; i <= edit.LineCount; i++) { var line = edit.Document.GetLineByNumber(i); string lineText = edit.Document.GetText(line.Offset, line.Length); if (CustomMarkdownSupport.IsImageLinkLine(lineText)) { var matchImageHeader = regImageOrFileHeader.Match(lineText); newFoldings.Add(new NewFolding(line.Offset, line.EndOffset) { Name = (matchImageHeader.Success && string.IsNullOrWhiteSpace(matchImageHeader.Value) == false) ? $"![{matchImageHeader.Value}]" : "![图像链接]", IsSpecial = true, }); } else if (CustomMarkdownSupport.IsFileLinkLine(lineText)) { var matchFileHeader = regImageOrFileHeader.Match(lineText); newFoldings.Add(new NewFolding(line.Offset, line.EndOffset) { Name = (matchFileHeader.Success && string.IsNullOrWhiteSpace(matchFileHeader.Value) == false) ? $"[{matchFileHeader.Value}]" : "[文件链接]", IsSpecial = true, }); } } #endregion newFoldings.Sort((a, b) => a.StartOffset.CompareTo(b.StartOffset)); return newFoldings; } /// <summary> /// 生成标题或任务列表被折叠时显示的文本。 /// </summary> /// <param name="sourceText">如果是 Header,只应传入“###”部分,不应包括后面的内容。</param> private string BuildHeaderOrTaskListItemHeader(string sourceText) { if (string.IsNullOrWhiteSpace(sourceText)) return ""; if (sourceText.StartsWith("[-]") || sourceText.StartsWith("[-]")) return "[-] "; if (sourceText.StartsWith("[+]")) return "[+] "; if (sourceText.StartsWith("[%]")) return "[%] "; if (sourceText.StartsWith("[#]")) return "[#] "; var level = sourceText.Length; switch (level) { case 1: return "Ⅰ ※ "; case 2: return "Ⅱ ※ "; case 3: return "Ⅲ ※ "; case 4: return "Ⅳ ※ "; case 5: return "Ⅴ ※ "; case 6: return "Ⅵ ※ "; default: return ""; } } /// <summary> /// 取标题或任务列表的文字部分(去除标志文本、空白字符等)。 /// </summary> /// <param name="src">源文本</param> public string GetContentOfHeaderOrTaskListItem(string src) { if (string.IsNullOrEmpty(src)) return ""; if (CustomMarkdownSupport.IsTaskLine(src)) return CustomMarkdownSupport.GetContentOfTaskListItem(src); if (src.StartsWith("#") == false) return ""; if (src.StartsWith("######")) return src.Substring(6); if (src.StartsWith("#####")) return src.Substring(5); if (src.StartsWith("####")) return src.Substring(4); if (src.StartsWith("###")) return src.Substring(3); if (src.StartsWith("##")) return src.Substring(2); if (src.StartsWith("#")) return src.Substring(1); return ""; } /// <summary> /// 取标题或任务列表的标志文本(不包含其它表示具体意思的文本)。 /// </summary> /// <param name="src">源文本</param> public string GetHeaderOfTitleOrTaskListItem(string src) { if (string.IsNullOrEmpty(src)) return ""; if (CustomMarkdownSupport.IsTaskLine(src)) { return CustomMarkdownSupport.GetHeaderOfTaskListItem(src); } if (src.StartsWith("#") == false) return ""; if (src.StartsWith("######")) return "######"; if (src.StartsWith("#####")) return "#####"; if (src.StartsWith("####")) return "####"; if (src.StartsWith("###")) return "###"; if (src.StartsWith("##")) return "##"; if (src.StartsWith("#")) return "#"; return ""; } } /// <summary> /// 提供六级标题的折叠功能时,此类用于记录某个标题的相关位置信息。 /// </summary> public class HeaderInfo { private string headerText = "..."; public string HeaderText { get { return this.headerText; } set { this.headerText = value; } } private string contentText = "#"; public string ContentText { get { return contentText; } set { this.contentText = value; } } private int length = 1; public int Length { get { return this.length; } set { this.length = value; } } private int offset = -1; public int Offset { get { return this.offset; } set { this.offset = value; } } private int endOffset = -1; public int EndOffset { get { return this.endOffset; } set { this.endOffset = value; } } } public class LineInfo { public DocumentLine Line { get; set; } public string LineText { get; set; } public string HeaderText { get; set; } } }