using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Windows; namespace LunarSF.SHomeWorkshop.LunarMarkdownEditor { public class CustomMarkdownSupport { /// <summary> /// 在使用 MarkDown Sharp 转换后,再对所有h1进行处理,自动生成jQuery函数,这样就可以支持点击标题显示、隐藏文本了。 /// </summary> /// <param name="htmlString">传入 Markdown Sharp 转换后的 Html 文本。</param> /// <returns></returns> public static string AppendProcessor(string htmlString, string directoryMark = "./", MainWindow.HtmlHeadersCollapseType htmlHeadersCollapse = MainWindow.HtmlHeadersCollapseType.No, bool fillBlankOn = true, bool compilePageMenu = false, string sourceFilePath = null, string previewPageLink = "", string nextPageLink = "") { if (string.IsNullOrEmpty(htmlString)) return ""; htmlString = FormatPreCodeBlock(htmlString); //载入jquery库。这东西无论如何都应载入,且只应载入一次。 StringBuilder javascriptBuilder = new StringBuilder(); //jQueryStringBuilder.Append("<script src=\"" + directoryMark + "json2.js\"></script>"); javascriptBuilder.Append("<script src=\"" + directoryMark + "jquery-1.7.0.min.js\"></script>" + //"<script src=\"" + directoryMark + "jquery.jqprint-0.3.js\"></script>" + (compilePageMenu ? "<script src=\"" + directoryMark + "menu_light.js\"></script>" : "") + "<script language=\"javascript\">" + "function print() {" + "$(\"#printArea\").jqprint({" + "importCSS: true" + "});" + "}" + "</script>"); //用于设置片段预览区的高度,以尽可能利用空间。 javascriptBuilder.Append("<script>function getHeight(){return document.body.scrollHeight;}</script>"); StringBuilder sbMenuText = new StringBuilder(); if (fillBlankOn) { #region 添加填空模式的js代码 javascriptBuilder.Append("<script>"); javascriptBuilder.Append("$(document).ready(function(){$('p>code').css('color', \"transparent\");});"); javascriptBuilder.Append("$(document).ready(function(){$('li>code').css('color', \"transparent\");});"); javascriptBuilder.Append("$(document).ready(function(){$('tr>code').css('color', \"transparent\");});"); javascriptBuilder.Append("$(document).ready(function(){$('.panel>code').css('color', \"transparent\");});");//注意:不能用“.panel code”。 javascriptBuilder.Append("$(document).ready(function() {"); javascriptBuilder.Append("$('p').click(function() {"); javascriptBuilder.Append("var mycss = document.getElementById('themeLink');"); javascriptBuilder.Append("if (mycss.getAttribute('href').indexOf('dark.css')>0)"); javascriptBuilder.Append("{"); javascriptBuilder.Append("$(this).find('code').css('color', 'yellow');"); javascriptBuilder.Append("}"); javascriptBuilder.Append("else {"); javascriptBuilder.Append("$(this).find('code').css('color', 'green');"); javascriptBuilder.Append("}"); javascriptBuilder.Append("});"); javascriptBuilder.Append("});"); javascriptBuilder.Append("$(document).ready(function() {"); javascriptBuilder.Append("$('li').click(function() {"); javascriptBuilder.Append("var mycss = document.getElementById('themeLink');"); javascriptBuilder.Append("if (mycss.getAttribute('href').indexOf('dark.css')>0)"); javascriptBuilder.Append("{"); javascriptBuilder.Append("$(this).find('code').css('color', 'yellow');"); javascriptBuilder.Append("}"); javascriptBuilder.Append("else {"); javascriptBuilder.Append("$(this).find('code').css('color', 'green');"); javascriptBuilder.Append("}"); javascriptBuilder.Append("});"); javascriptBuilder.Append("});"); javascriptBuilder.Append("$(document).ready(function() {"); javascriptBuilder.Append("$('tr').click(function() {"); javascriptBuilder.Append("var mycss = document.getElementById('themeLink');"); javascriptBuilder.Append("if (mycss.getAttribute('href').indexOf('dark.css')>0)"); javascriptBuilder.Append("{"); javascriptBuilder.Append("$(this).find('code').css('color', 'yellow');"); javascriptBuilder.Append("}"); javascriptBuilder.Append("else {"); javascriptBuilder.Append("$(this).find('code').css('color', 'green');"); javascriptBuilder.Append("}"); javascriptBuilder.Append("});"); javascriptBuilder.Append("});"); javascriptBuilder.Append("$(document).ready(function() {"); javascriptBuilder.Append("$('.panel').click(function() {"); javascriptBuilder.Append("var mycss = document.getElementById('themeLink');"); javascriptBuilder.Append("if (mycss.getAttribute('href').indexOf('dark.css')>0)"); javascriptBuilder.Append("{"); javascriptBuilder.Append("$(this).find('code').css('color', 'yellow');"); javascriptBuilder.Append("}"); javascriptBuilder.Append("else {"); javascriptBuilder.Append("$(this).find('code').css('color', 'green');"); javascriptBuilder.Append("}"); javascriptBuilder.Append("});"); javascriptBuilder.Append("});"); javascriptBuilder.Append("</script>"); #endregion } StringBuilder sb = new StringBuilder(); var lineStrings = htmlString.Split(new char[] { '\r', '\n' }); for (int index = 0; index < lineStrings.Length; index++) { #region 处理方块文本区域,不支持折叠 Regex regexStartS = new Regex(@"^<p>[ ]{0,3}\[([^\]]*)</p>"); var matchStartS = regexStartS.Match(lineStrings[index]); if (matchStartS != null && matchStartS.Success && lineStrings[index].Substring(matchStartS.Length).IndexOf(']') < 0) { lineStrings[index] = "<table border=\"0\" cellspacing=\"0\" class=\"square-block\">" + "<tbody border=\"0\">" + "<tr border=\"0\">" + "<td class=\"square-block-left\"/>" + "<td class=\"square-block-content\"><div>\n"; } else { Regex regexEnd = new Regex(@"^<p>[ ]{0,3}\].*</p>"); var matchEnd = regexEnd.Match(lineStrings[index]); if (matchEnd != null && matchEnd.Success) { lineStrings[index] = "\n</div></td>" + "<td class=\"square-block-right\"/>" + "</tr>" + "</tbody>" + "</table>"; } } sb.Append(lineStrings[index]); sb.Append("\n"); #endregion } htmlString = sb.ToString(); #region 解决相对路径的URL转义,有Bug。 //Regex aLinkHref = new Regex(@"(?<=(<[Aa] {1,}.*[hH][rR][eE][fF]=[""'])).*(?=[""'])"); //var matchesOfHrefs = aLinkHref.Matches(htmlString); //for (int i = matchesOfHrefs.Count - 1; i >= 0; i--) //{ // htmlString = htmlString.Substring(0, matchesOfHrefs[i].Index) + // UrlEncode(matchesOfHrefs[i].Value) + // htmlString.Substring(matchesOfHrefs[i].Index + matchesOfHrefs[i].Length); //} #endregion #region 新版的H1~H6六级标题折叠的代码 if (Globals.MainWindow.HtmlHeadersCollapse == MainWindow.HtmlHeadersCollapseType.Auto || Globals.MainWindow.HtmlHeadersCollapse == MainWindow.HtmlHeadersCollapseType.Manual) { List<HtmlHeaderInfo> infos = new List<HtmlHeaderInfo>(); var regexOfHeaders = new Regex(@"\<[hH][123456] ?.*\>.*\<\/[hH][123456]\>"); var matches = regexOfHeaders.Matches(htmlString); foreach (Match match in matches) { if (match != null && match.Success) { string header; int level = GetHtmlHeaderLevel(match.Value, out header); var info = new HtmlHeaderInfo() { Level = level, Header = header, Match = match, }; infos.Add(info); } } for (int i = 0; i < infos.Count; i++) { var info = infos[i]; for (int j = i - 1; j >= 0; j--) { var preInfo = infos[j]; if (preInfo.IsClosed == false && info.Level <= preInfo.Level) { info.PreviewHeaderPanelCloseDivsMark += "</div>"; preInfo.IsClosed = true; } } var panelIndex = i + 1; info.JavaScriptText = "<script>$(document).ready(function(){" + "$(\"#title" + panelIndex + "\").click(function(){" + "$(\"#panel" + panelIndex + "\").toggle();" + "if($(\"#panel" + panelIndex + "\").is(\":hidden\")){" + "$(\"#header_span" + panelIndex + "\").html('◆ ');}else{" + "$(\"#header_span" + panelIndex + "\").html('◇ ');}" + "});});</script>"; var autoHideText = ""; if (Globals.MainWindow.HtmlHeadersCollapse == MainWindow.HtmlHeadersCollapseType.Auto) { autoHideText = " style='display: none;'"; } var headerClass = Globals.MainWindow.CleanHeaders ? "clean" : "title"; //clean headers 仍然要支持折叠。 var hideMarkText = (Globals.MainWindow.HtmlHeadersCollapse == MainWindow.HtmlHeadersCollapseType.Manual ? "◇" : "◆"); info.Html = $"<h{info.Level} class='{headerClass}' id='title{panelIndex}' title='点击展开/折叠'><span id='header_span{panelIndex}' class='header_span'>{hideMarkText} </span>{info.Header}</h{info.Level}>"; info.NewText = info.PreviewHeaderPanelCloseDivsMark + info.Html + info.JavaScriptText + $"<div class='panel_title_h{info.Level}' id='panel{panelIndex}'{autoHideText}>"; } if (compilePageMenu) { for (int i = infos.Count - 1; i >= 0; i--) { //<span id="" class="anchor"></span> var gotoTopLinkText = ""; var info = infos[i]; if (i > 0 && compilePageMenu && info.Level == 1) { gotoTopLinkText = "<p class=\"back_to_top_link\"><a href=\"#__TOP_4E4ABC53-B143-46FF-93CF-F9381EAD8E14__\">回到顶部</a></p>"; //其实只需要使用 <a href="#">返回顶部</a> 就可以了,因为当 href 指向 # 时,表示指向网页自身。 } var anchorIdText = Guid.NewGuid().ToString(); var anchorText = $"<span id=\"{anchorIdText}\" class=\"anchor\"></span>"; htmlString = htmlString.Substring(0, info.Match.Index) + gotoTopLinkText + anchorText + info.NewText + htmlString.Substring(info.Match.Index + info.Match.Length); var prefixSpace = (info.Level == 1 ? "" : " "); sbMenuText.Insert(0, $"<p>{prefixSpace}<a href=\"#{anchorIdText}\">{info.Header}</a></p>"); //{new string(' ', Math.Max(0, info.Level - 1))}加上会有缩进效果,但右边栏宽度不够。 } } else { for (int i = infos.Count - 1; i >= 0; i--) { var info = infos[i]; htmlString = htmlString.Substring(0, info.Match.Index) + info.NewText + htmlString.Substring(info.Match.Index + info.Match.Length); } } for (int i = 0; i < infos.Count; i++) { if (infos[i].IsClosed == false) htmlString += "</div>"; } htmlString += "<script>$(document).ready(function() {" //折叠全部标题。 + "$('#file_header').click(function() {" + "$('.panel_title_h1,.panel_title_h2,.panel_title_h3,.panel_title_h4,.panel_title_h5,.panel_title_h6').hide();});" + "});</script>" + "<script>$(document).ready(function() {" + "$('#file_header').dblclick(function() {" + "$('.panel_title_h1,.panel_title_h2,.panel_title_h3,.panel_title_h4,.panel_title_h5,.panel_title_h6').show();});" + "});</script>"; } var menuHtml = ""; if (sbMenuText.Length > 0) { menuHtml = "<div id=\"left_menu\">" + "<div class=\"left_menu_content\">" + sbMenuText.ToString() + "<p><a href=\"#__TOP_4E4ABC53-B143-46FF-93CF-F9381EAD8E14__\">回到顶部</a></p>" //下面这行是编译整个工作区时才应该添加的“回到目录页”链接 + (string.IsNullOrWhiteSpace(sourceFilePath) == false ? ("<hr/><p>" + previewPageLink + " " + BuildIndexLink(sourceFilePath) + " " + nextPageLink + "</p>") : "") + "</div>" + "<strong class=\"left_menu_title\"><span> </span></strong>" + "</div>"; } return javascriptBuilder + menuHtml + htmlString; #endregion #region 旧版的H1折叠代码 //List<string> blockStrings = new List<string>(); //int index1 = 0; //int index2 = htmlString.IndexOf("<h1"); //while (index2 >= 0) //{ // blockStrings.Add(htmlString.Substring(index1, index2 - index1)); // index1 = index2; // index2 = htmlString.IndexOf("<h1", index2 + 1); //} //if (index1 >= 0) // blockStrings.Add(htmlString.Substring(index1)); //if (blockStrings.Count > 0) //{ // #region 添加折叠一级标题的js代码 // StringBuilder sBuilder = new StringBuilder(); // int panelCount = 0; // if (blockStrings.Count > 1)//第一个成员不是以<H1开头的。 // { // javascriptBuilder.Append("<script>"); // for (int i = 1; i <= blockStrings.Count; i++) // { // String s = blockStrings[i - 1]; // if (s.StartsWith("<h1") == false) // { // sBuilder.Append(s); // continue; // } // int indexOfH1End = s.IndexOf("</h1>"); // if (indexOfH1End < 0) // { // sBuilder.Append(s); // continue; // } // sBuilder.Append("<h1 "); // sBuilder.Append("id=\"title" + i + "\""); // sBuilder.Append(s.Substring(3, indexOfH1End + 2)); // sBuilder.Append("<div "); // sBuilder.Append("id=\"panel" + i + "\"" + (collapseH1 ? " style=\"display: none;\"" : "") + ">"); // sBuilder.Append(s.Substring(indexOfH1End + 5)); // sBuilder.Append("</div>"); // javascriptBuilder.Append("$(document).ready(function(){" // + "$(\"#title" + i + "\").click(function(){" // + "$(\"#panel" + i + "\").toggle();});});"); // //在页面加载后再隐藏不好,不如直接就不显示。 // //if (collapseH1) // //{ // // javascriptBuilder.Append("$(document).ready(function(){" // // + "$(\"#panel" + i + "\").toggle();});"); // //} // panelCount++; // } // } // #endregion // if (panelCount > 0) // { // return javascriptBuilder + "</script>" + sBuilder.ToString(); // } // return javascriptBuilder + htmlString; //} //else // return javascriptBuilder + htmlString; #endregion } /// <summary> /// 代码块中,"\r\n"会导致多一个换行。 /// </summary> /// <param name="htmlString"></param> /// <returns></returns> private static string FormatPreCodeBlock(string htmlString) { return htmlString.Replace("\n\n", "\n"); } /// <summary> /// 解决 URL 中特殊字符的转义问题。 /// 1. + URL 中 + 号表示空格 %2B /// 2. 空格 URL中的空格可以用 + 号或者编码 %20 /// 3. / 分隔目录和子目录 %2F /// 4. ? 分隔实际的 URL 和参数 %3F /// 5. % 指定特殊字符 %25 /// 6. # 表示书签 %23 /// 7. & URL 中指定的参数间的分隔符 %26 /// 8. = URL 中指定参数的值 %3D /// </summary> /// <param name="value">待转义的相对路径。</param> /// <returns>转义后的相对路径。</returns> public static string UrlEncode(string value) { var pieces = value.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); StringBuilder sb = new StringBuilder(); foreach (var s in pieces) { // % 必须第一个替换。 sb.Append(s.Replace("%", "%25").Replace("+", "%2B").Replace(" ", "%20").Replace("?", "%3F") .Replace("#", "%23").Replace("&", "%26").Replace("=", "%3D") + "/"); } var result = sb.ToString(); if (value.StartsWith("/") || value.StartsWith("\\")) { result = "/" + result; } if (value.EndsWith("/") || value.EndsWith("\\")) { result += "/"; } else { if (result.EndsWith("/") || result.EndsWith("\\")) { result = result.Substring(0, result.Length - 1); } } return result; } /// <summary> /// 取 Html 标题的层级,并传出去除标记符之外的文本。 /// </summary> /// <param name="htmlHeaderSpan">形如:“<h1>abc</h1>”这样的文本。</param> /// <param name="headerText">用以返回此级标题的内容文本(即 Html 首尾标签之间的文本)。</param> /// <returns></returns> private static int GetHtmlHeaderLevel(string htmlHeaderSpan, out string headerText) { if (string.IsNullOrWhiteSpace(htmlHeaderSpan)) { headerText = ""; return 0; } var reg = new Regex(@"\>.*\</"); Match match = reg.Match(htmlHeaderSpan); if (match != null && match.Success) { headerText = match.Value.Substring(1, match.Length - 3); } else { headerText = ""; } if (htmlHeaderSpan.StartsWith("<h1") || htmlHeaderSpan.StartsWith("<H1")) return 1; if (htmlHeaderSpan.StartsWith("<h2") || htmlHeaderSpan.StartsWith("<H2")) return 2; if (htmlHeaderSpan.StartsWith("<h3") || htmlHeaderSpan.StartsWith("<H3")) return 3; if (htmlHeaderSpan.StartsWith("<h4") || htmlHeaderSpan.StartsWith("<H4")) return 4; if (htmlHeaderSpan.StartsWith("<h5") || htmlHeaderSpan.StartsWith("<H5")) return 5; if (htmlHeaderSpan.StartsWith("<h6") || htmlHeaderSpan.StartsWith("<H6")) return 6; return 0; } /// <summary> /// 取任务列表项的标记文本(头文本)。 /// </summary> /// <param name="lineText">源文本。</param> /// <returns></returns> internal static string GetHeaderOfTaskListItem(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return ""; var tmp = lineText.Replace(" ", "").Replace("-", "-").Replace(" ", "").Replace("\t", "");//括弧间允许空白字符存在,所以不用TrimStart()。 if (tmp.ToLower().StartsWith("[-]") || tmp.ToLower().StartsWith("[%]") || tmp.ToLower().StartsWith("[+]") || tmp.ToLower().StartsWith("[#]")) { if (tmp.Length >= 4) { if (tmp[3] == '(') { if (tmp.Substring(4).Contains(')')) { //[]()形如这样是文字链接。 return ""; } } } return tmp.Substring(0, 3) + " "; } return ""; } /// <summary> /// 取六级标题的正则表达式。 /// </summary> /// <param name="level"></param> /// <returns></returns> public static string GetTieleRegByLevel(int level) { switch (level) { case 1: return "^[ ]{0,3}[##][^##]"; case 2: return "^[ ]{0,3}[##]{2}[^##]"; case 3: return "^[ ]{0,3}[##]{3}[^##]"; case 4: return "^[ ]{0,3}[##]{4}[^##]"; case 5: return "^[ ]{0,3}[##]{5}[^##]"; case 6: return "^[ ]{0,3}[##]{6}[^##]"; default: return ""; } } /// <summary> /// 判断此行文本是否“region”区域的分割线行。格式是:... region xxx ...。 /// </summary> /// <param name="lineText">源文本。</param> public static bool IsRegionSplitter(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; Regex reg = new Regex(@"^[ ]{0,3}[.。]{3,}[ \t]{0,}[rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,}.{0,}?[.。]{3,}[ \t]{0,}$"); var match = reg.Match(lineText); if (match != null && match.Success) { return true; } return false; } /// <summary> /// 判断指定的文本是不是 Header 标题。格式是:“###xxx”。 /// </summary> /// <param name="lineText">源文本。</param> public static bool IsHeaderLine(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; Regex reg = new Regex(@"^[ ]{0,3}[##].*$"); var match = reg.Match(lineText); if (match != null && match.Success) { return true; } return false; } /// <summary> /// 判断此行文本是否 TODO Comment。格式是:“; TODO: xxx”。 /// </summary> /// <param name="lineText"></param> /// <returns></returns> public static bool IsTodoCommentLine(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; Regex regex = new Regex(@"^[ ]{0,3}[;;]([tTTt][oOoO][dDdD][oOoO])?[::]"); var match = regex.Match(lineText); if (match != null && match.Success) { return true; } return false; } /// <summary> /// 判断此行文本是否 TODO Comment。格式类似:“; TODO: xxx”或“; DONE:xxx”等。 /// </summary> /// <param name="lineText">源文本。</param> /// <param name="todoCommentMark">用以传出 TODO Comment 的标记文本。</param> /// <param name="newTodoCommentTail">用以传出 TODO Comment 的内容文本(除标记以外的、有意义的文本)。</param> /// <param name="mark">传入“TODO”(或“DONE”等)。</param> public static bool IsTodoCommentLine(string lineText, out string todoCommentMark, out string newTodoCommentTail, string mark) { newTodoCommentTail = lineText; todoCommentMark = ""; if (string.IsNullOrWhiteSpace(lineText)) return false; Regex regex; switch (mark) { case "DOING": { regex = new Regex(@"^[ ]{0,3}[;;]([dDdD][oOoO][iIiI][nNnN][gGgG])?[::]"); break; } case "DONE": { regex = new Regex(@"^[ ]{0,3}[;;]([dDdD][oOoO][nNnN][eEeE])?[::]"); break; } default: { regex = new Regex(@"^[ ]{0,3}[;;]([tTTt][oOoO][dDdD][oOoO])?[::]"); break; } } var match = regex.Match(lineText); if (match != null && match.Success) { todoCommentMark = match.Value; newTodoCommentTail = " " + lineText.Substring(match.Length).Trim(new char[] { ' ', ' ', '\t' }); return true; } return false; } /// <summary> /// 判断此行文本是否编译菜单链接的指示标记文本行。格式是:“; [Menu]: xxx”。 /// </summary> /// <param name="lineText">源文本。</param> /// <param name="newTodoCommentTail">用以传出 [Menu]: 后的内容文本(除标记以外的、可能有意义的注释文本)。</param> public static bool IsMenuMarkLine(string lineText, out string newMenuMarkTail) { newMenuMarkTail = lineText; if (string.IsNullOrWhiteSpace(lineText)) return false; Regex regex = new Regex(@"^[ ]{0,3}[;;](\[?(([mMmM][eEeE][nNnN][uUuU])|(菜单))\]?)[::]"); var match = regex.Match(lineText); if (match != null && match.Success) { newMenuMarkTail = " " + lineText.Substring(match.Length).Trim(new char[] { ' ', ' ', '\t' }); return true; } return false; } /// <summary> /// 判断此行文本是否用于指示当前文档演示模式的文本行。格式是:“;PM:xxx”。 /// </summary> /// <param name="lineText"></param> /// <param name="newPresentationTypeTail"></param> /// <returns></returns> private static bool IsPresentationTypeLine(string lineText, out string newPresentationTypeTail) { newPresentationTypeTail = lineText; if (string.IsNullOrWhiteSpace(lineText)) return false; Regex regex = new Regex(@"^[ ]{0,3}[;;](([pPpP][mMmM])|(演示(模式)?))[::]"); var match = regex.Match(lineText); if (match != null && match.Success) { newPresentationTypeTail = " " + lineText.Substring(match.Length).Trim(new char[] { ' ', ' ', '\t' }); return true; } return false; } /// <summary> /// 判断此行是否被嵌入到 引用块 中的 Header 标题。 /// </summary> /// <param name="lineText">源文本。</param> /// <param name="match">正则匹配对象。</param> public static bool IsHeaderInBlockQuote(string lineText, out Match match) { if (string.IsNullOrWhiteSpace(lineText)) { match = null; return false; } Regex reg = new Regex(@"^[ ]{0,3}[>〉》][>〉》 ][##]{1,6}"); match = reg.Match(lineText); if (match != null && match.Success) { return true; } match = null; return false; } /// <summary> /// 取任务列表项的内容文本(即形如:“[-]xxx”中的“xxx”部分。 /// </summary> /// <param name="lineText">源文本。</param> internal static string GetContentOfTaskListItem(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return ""; var tmp = lineText.Replace(" ", "").Replace("-", "-").Replace(" ", "").Replace("\t", "");//括弧间允许空白字符存在,所以不用TrimStart()。 if (tmp.ToLower().StartsWith("[-]") || tmp.ToLower().StartsWith("[%]") || tmp.ToLower().StartsWith("[+]") || tmp.ToLower().StartsWith("[#]")) { if (tmp.Length >= 4) { if (tmp[3] == '(') { if (tmp.Substring(4).Contains(')')) { //[]()形如这样是文字链接。 return ""; } } } return tmp.Substring(3).TrimStart(new char[] { ' ' }); } return ""; } /// <summary> /// 此方法用于在二维文字表的单元格文本、任务列表项的正文文本、材料出处文本、 /// 放大显示的文本行、自定义折叠区的首尾文本、步骤标记的正文文本、冒号开头的注释文本……等自定义元素中支持 Markdown 基本格式语法(加粗、倾斜等) /// 此方法受制于 Globals.MainWindow.EnableBaseMDSyntax 属性的值,这个属性的值记录在工作区而不是应用程序的配置文件中,默认值为 false,即不开启。 /// 之所以默认不开启,是因为对过去的旧文件可能造成破坏。 /// </summary> /// <returns></returns> private static String FormatLineContent(ref MarkdownSharp.Markdown md, string srcLineContent) { if (string.IsNullOrEmpty(srcLineContent)) return ""; if (md == null) return srcLineContent; if (Globals.MainWindow.EnableBaseMDSyntax == false) return srcLineContent; //加“正文”两个字的目的是防止解析为引用块、列表项等特殊块文本。 //只要你喜欢,换成其它普通文本也是可以的——只要别让 MarkdownSharp 的转换器发疯就可以。 var resultContent = md.Transform("正文" + srcLineContent.Trim(new char[] { ' ', ' ' })); if (resultContent.ToLower().StartsWith("<p>正文")) resultContent = resultContent.Substring(5); else if (resultContent.ToLower().StartsWith("<p>")) resultContent = resultContent.Substring(3); if (resultContent.ToLower().EndsWith("</p>")) resultContent = resultContent.Substring(0, resultContent.Length - 4); else if (resultContent.ToLower().EndsWith("</p>\n")) resultContent = resultContent.Substring(0, resultContent.Length - 5); return resultContent; } /// <summary> /// 预处理markdown文本,这样可以支持表格、块引用预处理等。 /// /// |表格标题| |列标题1|列标题2|列标题3| /// |:----:|--:|:--| /// |列1、行1|列2、行1|列3、行1| /// |列1、行2|列2、行2|列3、行3| /// </summary> /// <param name="markdownText">待转换的源 Markdown 文本。</param> /// <param name="separatorText">用于分割文本行的分割符。这是考虑文本文件可能来自于不同的操作系统。</param> /// <param name="startH1Number">用以表示H1应从什么值开始编号。这是为跨文件接续编号准备的。</param> /// <param name="forbiddenAutoNumber">强行禁止自动编号。</param> /// <returns></returns> public static String PreProcessor(string markdownText, int startH1Number, bool forbiddenAutoNumber, char[] separatorText) { if (markdownText == null || markdownText.Length == 0) { return markdownText; } markdownText = Question.ConvertQuestionsToHtml(markdownText);//先将试题文本转换为html标签。 String[] lineStrings = markdownText.Split(separatorText, StringSplitOptions.None); #region 格式化引用块 StringBuilder sb = new StringBuilder(); for (int i = 0; i < lineStrings.Length; i++) { var text = lineStrings[i]; int markIndex = -1; bool isBlockQuote = true; if (string.IsNullOrEmpty(text)) { isBlockQuote = false; } else { for (int j = 0; j < text.Length; j++) { var c = text[j]; if (c == ' ' || c == ' ') continue; else if (c == '>' || c == '〉' || c == '》') { markIndex = j; continue; } isBlockQuote = false; break; } } if (isBlockQuote && markIndex >= 0 && markIndex <= 3) { var ts = lineStrings[i].TrimStart(new char[] { '\t', ' ', ' ' }); if (ts.StartsWith("》 ") || ts.StartsWith("》 ") || ts.StartsWith("〉 ") || ts.StartsWith("〉 ")) { lineStrings[i] = "> " + ts.Substring(2); } else if (ts.StartsWith("》") || ts.StartsWith("〉") || ts.StartsWith(">")) { lineStrings[i] = "> " + ts.Substring(1); } } } #endregion markdownText = sb.ToString(); MarkdownSharp.Markdown md = new MarkdownSharp.Markdown(); #region 格式化表格 List<TableLinesInfo> tableSourcesList = new List<TableLinesInfo>(); TableLinesInfo tlInfo = null; for (int i = 0; i < lineStrings.Length; i++) { String lineString = lineStrings[i]; //表格行第一字符可以不是|或|,但必须是非空字符。 if (IsTableRow(lineString)) { if (lineString.StartsWith("|") == false && lineString.StartsWith("|") == false) { lineString = "|" + lineString; } //这个移到TableLinesInfo.PreviewFormatTable()方法中去了。 //if (lineString.EndsWith("|") == false && lineString.EndsWith("|") == false) //{ // lineString = lineString + "|"; //} } if (lineString.StartsWith("|") || lineString.StartsWith("|")) // && (lineString.EndsWith("|") || lineString.EndsWith("|"))) { if (tlInfo == null) { tlInfo = new TableLinesInfo(); tlInfo.StartLineIndex = i; } if (lineString.EndsWith("|^") || lineString.EndsWith("|^")) { tlInfo.TableLines.Add(new TableLine() { LineText = lineString, Type = TableLineType.MergeLine, }); } else { tlInfo.TableLines.Add(new TableLine() { LineText = lineString, Type = TableLineType.Normal, }); } } else { if (tlInfo != null) { tlInfo.EndLineIndex = (i - 1); tableSourcesList.Add(tlInfo); tlInfo = null; } } } // 如果最后一行就是表格最后一行 if (tlInfo != null) { tlInfo.EndLineIndex = (lineStrings.Length - 1); tableSourcesList.Add(tlInfo); tlInfo = null; } foreach (var info in tableSourcesList) { info.PreviewFormatTableLines(); } for (int tableIndex = 0; tableIndex < tableSourcesList.Count; tableIndex++) { TableLinesInfo tableLinesInfo = tableSourcesList[tableIndex]; int maxColumnsCount = 0; String definition = ""; int definitionLineIndex = -1; bool hasColumnDefinitionLine = false; for (int j = 0; j < tableLinesInfo.TableLines.Count; j++) { var tableLine = tableLinesInfo.TableLines[j]; String lineString = tableLine.LineText; if (tableLine.Type == TableLineType.ColumnDefinitionLine) { definition = lineString; definitionLineIndex = j; definition = definition.Replace("|", "\r\n") .Replace("|", "\r\n").Replace(":", ":") .Replace("-", "-").Replace(" ", "") .Replace(" ", "").Replace("\t", ""); String[] definitionTexts = definition.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (String definitionText in definitionTexts) { if (definitionText.Length == 0) continue; bool left = false; bool right = false; if (definitionText.StartsWith(":") || definitionText.StartsWith("^:")) left = true; if (definitionText.EndsWith(":") || definitionText.EndsWith(":^")) right = true; ColumnAlignment cDefinition; if (left && right) { left = right = false; cDefinition = ColumnAlignment.CENTER; } else if (right) { cDefinition = ColumnAlignment.RIGHT; } else { cDefinition = ColumnAlignment.LEFT; } tableLinesInfo.ColumnAlignments.Add(cDefinition); } hasColumnDefinitionLine = true; } else { var commonLine = lineString.Replace("|", "\t").Replace("|", "\t"); if (commonLine.EndsWith("\t")) { commonLine = commonLine.Substring(0, commonLine.Length - 1); } if (commonLine.StartsWith("\t")) { commonLine = commonLine.Substring(1); } String[] spanStrings = commonLine.Split('\t'); maxColumnsCount = Math.Max(maxColumnsCount, spanStrings.Length); } }// 取出表的各列定义(主要是对齐) // 如果没有定义,则自动定义列(取最多的一行定义)。 if (false == hasColumnDefinitionLine) { for (int j = 0; j < tableLinesInfo.TableLines.Count; j++) { String lineString = tableLinesInfo.TableLines[j].LineText .Replace("|", "\t").Replace("|", "\t"); if (lineString.EndsWith("\t")) { lineString = lineString.Substring(0, lineString.Length - 1); } if (lineString.StartsWith("\t")) { lineString = lineString.Substring(1); } String[] spanStrings = lineString.Split('\t'); maxColumnsCount = Math.Max(maxColumnsCount, spanStrings.Length); } for (int i = 1; i <= maxColumnsCount; i++) { tableLinesInfo.ColumnAlignments.Add(ColumnAlignment.LEFT);// 默认全左齐。 } } StringBuilder tableBuilder = new StringBuilder(); if (Globals.MainWindow.TableCaptionAtBottom) { tableBuilder.Append("<table style=\"margin-bottom:0.2em;\">"); } else tableBuilder.Append("<table>"); var tableCaptionHtml = ""; for (int tableLineIndex = 0; (tableLineIndex < definitionLineIndex && tableLineIndex < tableLinesInfo .TableLines.Count); tableLineIndex++) { // 表头部分 String lineString = tableLinesInfo.TableLines[tableLineIndex].LineText; Regex regex = new Regex(@"[ \t]{1,}"); lineString = regex.Replace(lineString, " "); Regex regex2 = new Regex(@"[ \t]{0,}[||][ \t]{0,}"); lineString = regex2.Replace(lineString, "|"); if (lineString.StartsWith("|") || lineString.StartsWith("|")) lineString = lineString.Substring(1); if (lineString.EndsWith("|") || lineString.EndsWith("|")) lineString.Substring(0, lineString.Length - 2); lineString = lineString.Replace("|", "\t").Replace("|", "\t"); // 去除头尾。 if (lineString.EndsWith("\t")) { lineString = lineString.Substring(0, lineString.Length - 1); } String[] spanStrings = lineString.Split('\t'); if (spanStrings.Length > 0) { if (tableLineIndex == 0 && spanStrings.Length == 1) { if (Globals.MainWindow.TableCaptionAtBottom == false) { tableCaptionHtml = $"<caption>{spanStrings[0]}</caption>\n"; tableBuilder.Append(tableCaptionHtml); } else { tableCaptionHtml = $"<p style=\"margin-bottom:1em;margin-top:0;text-indent:0;text-align:center;font-size:1em;font-weight:bold;\">表{tableIndex + 1} {spanStrings[0]}</p>\n"; //加到末尾。 } continue; } tableBuilder.Append("<tr>"); for (int spanIndex = 0; spanIndex < Math.Max(Math.Max(spanStrings.Length, tableLinesInfo.ColumnAlignments.Count), maxColumnsCount); spanIndex++) { if (spanIndex >= spanStrings.Length) { //感觉表头还是全部居中比较好看。 tableBuilder.Append("<th style=\"TEXT-ALIGN: center;\">"); tableBuilder.Append("</th>"); // 添加空单元格。 continue;// 防止用户一行末尾未填空内容。 } string s = ConvertChar(spanStrings[spanIndex]); //感觉表头还是全部居中对齐比较好看。 tableBuilder.Append("<th style=\"TEXT-ALIGN: center;\">"); tableBuilder.Append(s == "" ? " " : FormatLineContent(ref md, s)); tableBuilder.Append("</th>"); } tableBuilder.Append("</tr>"); } } for (int index = definitionLineIndex + 1; index < tableLinesInfo .TableLines.Count; index++) { // 表体部分 String lineString = tableLinesInfo.TableLines[index].LineText; Regex regex1 = new Regex(@"[ \t]{1,}"); lineString = regex1.Replace(lineString, " "); Regex regex2 = new Regex(@"[ \t]{0,}[||][ \t]{0,}"); lineString = regex2.Replace(lineString, "|"); if (lineString.StartsWith("|") || lineString.StartsWith("|")) lineString = lineString.Substring(1); if (lineString.EndsWith("|") || lineString.EndsWith("|")) lineString = lineString.Substring(0, lineString.Length - 1); lineString = lineString.Replace("|", "\t").Replace("|", "\t"); // 去除头尾。 if (lineString.EndsWith("\t")) { lineString = lineString.Substring(0, lineString.Length - 1); } String[] spanStrings = lineString.Split('\t'); if (spanStrings.Length > 0) { tableBuilder.Append("<tr>"); for (int spanIndex = 0; spanIndex < Math.Max(Math.Max(spanStrings.Length, tableLinesInfo.ColumnAlignments.Count), maxColumnsCount); spanIndex++) { //这行补齐会造成错误 //if (spanIndex >= tableLinesInfo.ColumnAlignments.Count) //{ // tableBuilder.Append("<td>"); //} if (spanIndex >= spanStrings.Length) { // 要取出对齐 ColumnAlignment align = GetColumnAlign(tableLinesInfo.ColumnAlignments, spanIndex); switch (align) { case ColumnAlignment.CENTER: { tableBuilder .Append("<td style=\"TEXT-ALIGN: center;\">"); break; } case ColumnAlignment.RIGHT: { tableBuilder .Append("<td style=\"TEXT-ALIGN: right;\">"); break; } default: tableBuilder .Append("<td style=\"TEXT-ALIGN: left;\">"); break; } tableBuilder.Append(" </td>\n"); // 添加内容为一个全角空格的单元格。 continue;// 防止用户一行末尾未填空内容。 } string s = ConvertChar(spanStrings[spanIndex]); // 要取出对齐 ColumnAlignment align2 = GetColumnAlign(tableLinesInfo.ColumnAlignments, spanIndex); switch (align2) { case ColumnAlignment.CENTER: { tableBuilder .Append("<td style=\"TEXT-ALIGN: center;\">"); break; } case ColumnAlignment.RIGHT: { tableBuilder .Append("<td style=\"TEXT-ALIGN: right;\">"); break; } default: tableBuilder .Append("<td style=\"TEXT-ALIGN: left;\">"); break; } tableBuilder.Append(s == "" ? " " : FormatLineContent(ref md, s)); //单元格的内容 tableBuilder.Append("</td>\n"); } tableBuilder.Append("</tr>\n"); } } tableBuilder.Append("</table>\n"); if (Globals.MainWindow.TableCaptionAtBottom) { tableBuilder.Append(tableCaptionHtml); } tableLinesInfo.TableHtmlText = tableBuilder.ToString(); } #endregion 转换二维文字表 int regionHeaderNumber = 0; //这两个用来处理自定义折叠区 int regionTailNumber = 0; Stack<int> panelLayerStack = new Stack<int>(); //此变量只用于记录自定义折叠区嵌套的层数,不是直接的序列 #region 处理六级标题,自动添加数字序号 var levelIndex1 = startH1Number - 1; var levelIndex2 = 0; var levelIndex3 = 0; var levelIndex4 = 0; var levelIndex5 = 0; var levelIndex6 = 0; #endregion // 拼接。 StringBuilder sBuilder = new StringBuilder(); int imageIndex = 1; for (int index = 0; index < lineStrings.Length; index++) { if (string.IsNullOrWhiteSpace(lineStrings[index])) continue; //处理删除线标记(代码块中不转变)。 if (lineStrings[index].StartsWith(" ") == false && lineStrings[index].StartsWith("\t") == false) { lineStrings[index] = lineStrings[index].Replace("[=", "<s>").Replace("[=", "<s>").Replace("【=", "<s>").Replace("【=", "<s>") .Replace("=]", "</s>").Replace("=】", "</s>").Replace("=】", "</s>").Replace("=]", "</s>") .Replace("[\\=", "[=").Replace("[\\=", "[=").Replace("【\\=", "[=").Replace("【\\=", "[=") .Replace("=\\]", "=]").Replace("=\\】", "=]").Replace("=\\】", "=]").Replace("=\\]", "=]"); } #region 处理单独成行的图像链接文本 var trimChars = new char[] { ' ', ' ', '\t' }; //对于单独一行的图像链接,自动添加“图 x 图像标题”这样的文本 if (IsImageLinkLine(lineStrings[index])) { var regex = new Regex(@"(?<=(!\[)).*(?=\])"); var match = regex.Match(lineStrings[index]); if (match.Success && string.IsNullOrEmpty(match.Value) == false) { var imgTitle = match.Value.Trim(trimChars); if (Globals.MainWindow.ImageTitleAtTop) { sBuilder.Append($"<p style=\"text-indent:0px;text-align:center;font-weight:bold;margin-top:1em;margin-bottom:0.2em;\">图{imageIndex++} {imgTitle}</p>\r\n{lineStrings[index]}"); } else { sBuilder.Append($"{lineStrings[index]}\r\n<p style=\"text-indent:0px;text-align:center;font-weight:bold;margin-top:0.2em;margin-bottom:1em;\">图{imageIndex++} {imgTitle}</p>"); } sBuilder.Append("\r\n"); continue; } } #endregion #region 处理六级标题,自动添加数字序号 if (Globals.MainWindow.AutoNumberHeaders && forbiddenAutoNumber == false) { string titleHeader; string titleTail; var titleLevel = HeaderLevel(lineStrings[index], out titleHeader, out titleTail); //不需要在这里考虑是否编译纯净(Clean)版的标题,在AppendProcess中自然会添加class='clean'。 switch (titleLevel) { case 1: { levelIndex1++; levelIndex2 = levelIndex3 = levelIndex4 = levelIndex5 = levelIndex6 = 0; lineStrings[index] = "<h1>" + //titleHeader + BuildHeaderIndexText(levelIndex1, levelIndex2, levelIndex3, levelIndex4, levelIndex5, levelIndex6) + titleTail + "</h1>"; //不宜开放Markdown基本格式,因为<code></code>块的填空效果与标题的折叠展开操作冲突。 break; } case 2: { levelIndex2++; levelIndex3 = levelIndex4 = levelIndex5 = levelIndex6 = 0; lineStrings[index] = "<h2>" + //titleHeader + BuildHeaderIndexText(levelIndex1, levelIndex2, levelIndex3, levelIndex4, levelIndex5, levelIndex6) + titleTail + "</h2>"; //不宜开放Markdown基本格式,因为<code></code>块的填空效果与标题的折叠展开操作冲突。 break; } case 3: { levelIndex3++; levelIndex4 = levelIndex5 = levelIndex6 = 0; lineStrings[index] = "<h3>" + //titleHeader + BuildHeaderIndexText(levelIndex1, levelIndex2, levelIndex3, levelIndex4, levelIndex5, levelIndex6) + titleTail + "</h3>"; //不宜开放Markdown基本格式,因为<code></code>块的填空效果与标题的折叠展开操作冲突。 break; } case 4: { levelIndex4++; levelIndex5 = levelIndex6 = 0; lineStrings[index] = "<h4>" + //titleHeader + BuildHeaderIndexText(levelIndex1, levelIndex2, levelIndex3, levelIndex4, levelIndex5, levelIndex6) + titleTail + "</h4>"; //不宜开放Markdown基本格式,因为<code></code>块的填空效果与标题的折叠展开操作冲突。 break; } case 5: { levelIndex5++; levelIndex6 = 0; lineStrings[index] = "<h5>" + //titleHeader + BuildHeaderIndexText(levelIndex1, levelIndex2, levelIndex3, levelIndex4, levelIndex5, levelIndex6) + titleTail + "</h5>"; //不宜开放Markdown基本格式,因为<code></code>块的填空效果与标题的折叠展开操作冲突。 break; } case 6: { levelIndex6++; lineStrings[index] = "<h6>" + //titleHeader + BuildHeaderIndexText(levelIndex1, levelIndex2, levelIndex3, levelIndex4, levelIndex5, levelIndex6) + titleTail + "</h6>"; //不宜开放Markdown基本格式,因为<code></code>块的填空效果与标题的折叠展开操作冲突。 break; } } } #endregion #region 处理自定义折叠区域 Regex regexStart = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,}?)?([!?!?IiWwEeQqIiWwEeQq][ \t]*)?\{"); var matchStart = regexStart.Match(lineStrings[index]); if (matchStart != null && matchStart.Success && lineStrings[index].Substring(matchStart.Length).IndexOf('}') < 0) { regionHeaderNumber++;//要先加才能保持尾部索引一致 panelLayerStack.Push(regionHeaderNumber); var regionHeaderText = lineStrings[index].Substring(matchStart.Length); var hideBottomBorderStyleText = ""; if (string.IsNullOrWhiteSpace(regionHeaderText.Replace(" ", " "))) { hideBottomBorderStyleText = "border-bottom-color:transparent;border-bottom-width:0;"; regionHeaderText = "";//不再采用两个全角空格强制显示的办法,这样更节省空间。 } var regionClassText = "region"; Regex regexStartI = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?([IiIi][ \t]{0,})\{"); if (regexStartI.Match(lineStrings[index]).Success) { hideBottomBorderStyleText += "padding-left: 1em;"; hideBottomBorderStyleText += "font-weight : bold;"; hideBottomBorderStyleText += "text-indent: 0px;"; hideBottomBorderStyleText += "text-align: left;"; regionClassText = "region_i"; } Regex regexStartW = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?([WwWw!!][ \t]{0,})\{"); if (regexStartW.Match(lineStrings[index]).Success) { hideBottomBorderStyleText += "padding-left: 1em;"; hideBottomBorderStyleText += "font-weight : bold;"; hideBottomBorderStyleText += "text-indent: 0px;"; hideBottomBorderStyleText += "text-align: left;"; regionClassText = "region_w"; } Regex regexStartE = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?([EeEe][ \t]{0,})\{"); if (regexStartE.Match(lineStrings[index]).Success) { hideBottomBorderStyleText += "padding-left: 1em;"; hideBottomBorderStyleText += "font-weight : bold;"; hideBottomBorderStyleText += "text-indent: 0px;"; hideBottomBorderStyleText += "text-align: left;"; regionClassText = "region_e"; } Regex regexStartQ = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?([QqQq??][ \t]{0,})\{"); if (regexStartQ.Match(lineStrings[index]).Success) { hideBottomBorderStyleText += "padding-left: 1em;"; hideBottomBorderStyleText += "font-weight : bold;"; hideBottomBorderStyleText += "text-align: left;"; hideBottomBorderStyleText += "text-indent: 0px;"; regionClassText = "region_q"; } hideBottomBorderStyleText = $"style='{hideBottomBorderStyleText}'"; lineStrings[index] = $"<div class='{regionClassText}'><p class='region_header' id='region_header_{regionHeaderNumber}'{hideBottomBorderStyleText}>{FormatLineContent(ref md, regionHeaderText)}</p><div class='region_panel' id='region_panel_{regionHeaderNumber}'><table><tr><td>"; lineStrings[index] += "<script>$(document).ready(function() {" + $@"$('#region\_header\_{regionHeaderNumber}').click(function() {{" + $@"$('#region\_panel\_{regionHeaderNumber}').toggle();" + "});" + "}); </script><p>"; } else { regionTailNumber++; Regex regexEnd = new Regex(@"^[ ]{0,3}\}[ ]{0,}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ ]{0,})?"); var matchEnd = regexEnd.Match(lineStrings[index]); if (matchEnd != null && matchEnd.Success) { var regionTailText = lineStrings[index].Substring(matchEnd.Length); var hideTopBorderStyleText = ""; if (string.IsNullOrWhiteSpace(regionTailText.Replace(" ", " "))) { hideTopBorderStyleText = " style='border-top-color:transparent;border-bottom-width:0;'"; regionTailText = "";//不再采用两个全角空格强制显示的办法,这样更节省空间。 } int panelNumber = 0; if (panelLayerStack.Count > 0) { panelNumber = panelLayerStack.Pop(); } lineStrings[index] = $"</p></td></tr></table>\n</div><p class='region_tail' id='region_tail_{panelNumber}'{hideTopBorderStyleText}>{FormatLineContent(ref md, regionTailText)}</p></div>"; lineStrings[index] += "<script>$(document).ready(function() {" + $@"$('#region\_tail\_{panelNumber}').click(function() {{" + $@"$('#region\_panel\_{panelNumber}').toggle();" + "});" + "}); </script>"; } else { Regex regexSplitter = new Regex(@"^[ ]{0,3}[.。]{3,}[ \t]{0,}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?.{0,}[.。]{3,}[ \t]{0,}$"); //这行中必须有:...region...,此行为分栏符。 var matchSplitter = regexSplitter.Match(lineStrings[index]); if (matchSplitter != null && matchSplitter.Success) { lineStrings[index] = "</td><td>"; } } } #endregion #region 处理冒号开头的注释文本 if (lineStrings[index].StartsWith(":") || lineStrings[index].StartsWith(":")) { //以冒号开头的,作为备注。 sBuilder.Append($"<p class='comment'>{FormatLineContent(ref md, lineStrings[index].Substring(1))}</p>\r\n"); sBuilder.Append("\r\n"); continue; } #endregion #region 处理材料出处文本 Regex regexMaterialSource = new Regex(@"^[ ]{0,3}([>》〉]{1,1}[>》〉 \t]{0,}){0,}——[ \t]{0,}《.*》.{0,}$"); Match matchMaterialSource = regexMaterialSource.Match(lineStrings[index]); if (matchMaterialSource != null && matchMaterialSource.Success) { Regex regexBlockQuoter = new Regex(@"^[ ]{0,3}[>》〉]{1,1}[>》〉 \t]{0,}"); var blockQuoterHeader = ""; var matchBlockQuoter = regexBlockQuoter.Match(lineStrings[index]); string lineTailTextOfBlockQuoter; if (matchBlockQuoter != null && matchBlockQuoter.Success) { blockQuoterHeader = lineStrings[index].Substring(0, matchBlockQuoter.Length) .Replace("》", ">").Replace("〉", ">") .Replace(" ", "").Replace(" ", "").Replace("\t", "").Replace(">", "> "); lineTailTextOfBlockQuoter = lineStrings[index].Substring(matchBlockQuoter.Length).Trim(new char[] { ' ', ' ', '\t' }); } else { lineTailTextOfBlockQuoter = lineStrings[index].Trim(new char[] { ' ', ' ', '\t' }); } Regex regexMaterialSourceStartMark = new Regex(@"——[ \t]{0,}《"); Match matchMaterialSourceStartMark = regexMaterialSource.Match(lineTailTextOfBlockQuoter); if (matchMaterialSourceStartMark != null && matchMaterialSourceStartMark.Success) { lineTailTextOfBlockQuoter = regexMaterialSourceStartMark.Replace(lineTailTextOfBlockQuoter, "——《"); } sBuilder.Append($"{blockQuoterHeader}<p class='ex_m_cc'>{FormatLineContent(ref md, lineTailTextOfBlockQuoter)}</p>\r\n"); sBuilder.Append("\r\n"); continue; } #endregion #region 处理任务列表项 if (lineStrings[index].StartsWith("[-]") || lineStrings[index].StartsWith("[-]")) { //以[-]开头的,作为尚未开始的任务列表项目。 sBuilder.Append($"<p class=\"task\"><span class='task_u'>[-]</span> <span class='task_up'>{FormatLineContent(ref md, lineStrings[index].Substring(4))}</span></p>"); sBuilder.Append("\r\n"); continue; } if (lineStrings[index].StartsWith("[%]")) { //以[%]开头的,作为正在进行的任务列表项目。 sBuilder.Append($"<p class=\"task\"><span class='task_p'>[%]</span> <span class='task_pp'>{FormatLineContent(ref md, lineStrings[index].Substring(4))}</span></p>"); sBuilder.Append("\r\n"); continue; } if (lineStrings[index].StartsWith("[+]")) { //以[+]开头的,作为已完成任务列表项目。 sBuilder.Append($"<p class=\"task\"><span class='task_f'>[+]</span> <span class='task_fp'><s>{FormatLineContent(ref md, lineStrings[index].Substring(4))}</s></span></p>"); sBuilder.Append("\r\n"); continue; } if (lineStrings[index].StartsWith("[#]")) { //以[#]开头的,作为已废弃任务列表项目。 sBuilder.Append($"<p class=\"task\"><span class='task_a'>[#]</span> <span class='task_ap'><s>{FormatLineContent(ref md, lineStrings[index].Substring(4))}</s></span></p>"); sBuilder.Append("\r\n"); continue; } if (IsDateLine(lineStrings[index])) { var className = "date"; var indexStatus = -1; indexStatus = lineStrings[index].IndexOf("][-]"); if (indexStatus >= 0) className = "date_u"; else { indexStatus = lineStrings[index].IndexOf("][%]"); if (indexStatus >= 0) { className = "date_p"; } else { indexStatus = lineStrings[index].IndexOf("][+]"); if (indexStatus >= 0) { className = "date_f"; } else { indexStatus = lineStrings[index].IndexOf("][#]"); if (indexStatus >= 0) { className = "date_a"; } } } } if (indexStatus >= 0) { sBuilder.Append("<p><span class='" + className + $"'>{FormatLineContent(ref md, lineStrings[index].Substring(0, indexStatus + 4))}</span>{lineStrings[index].Substring(indexStatus + 5)}</p>"); sBuilder.Append("\r\n"); continue; } var indexOfSquareBracket = lineStrings[index].IndexOf("]"); if (indexOfSquareBracket < 0) { sBuilder.Append(lineStrings[index]); sBuilder.Append("\r\n"); continue; } else { sBuilder.Append($"<p><span class='date'>{FormatLineContent(ref md, lineStrings[index].Substring(0, indexOfSquareBracket + 1))}</span>{lineStrings[index].Substring(indexOfSquareBracket + 1)}</p>"); sBuilder.Append("\r\n"); continue; } } #endregion #region 处理应放大显示的文本行 Regex regexMuiltyText = new Regex(@"^[ ]{0,3}<[12345678901234567890]{1,}([.。][12345678901234567890]{1,}){0,}>"); var matchMuiltyText = regexMuiltyText.Match(lineStrings[index]); if (matchMuiltyText.Success) { double muilti; if (double.TryParse(matchMuiltyText.Value.Substring(1, matchMuiltyText.Length - 2), out muilti) == false) { muilti = 1; } if (muilti < 1) muilti = 1; if (muilti > 10) muilti = 10; var line = $"<div class=\"multiple-text\"><p style=\"font-size:{muilti}em;line-height:1.5;text-align:center;margin:0;\">{FormatLineContent(ref md, lineStrings[index].Substring(matchMuiltyText.Length))}</p></div>"; sBuilder.Append(line); sBuilder.Append("\r\n"); continue; } #endregion #region 处理步骤标记 Regex regexStepLine = new Regex(@"^[ ]{0,3}\[[ ]{0,}[sSsS]([tTtT][eEeE][pPpP]){0,1}[ ]{0,}(([bBbB]([eEeE][gGgG][iIiI][nNnN]){0,1})|([eEeE]([nNnN][dDdD]){0,1})){0,1}\]"); var matchStepLine = regexStepLine.Match(lineStrings[index]); if (matchStepLine.Success) { var tail = lineStrings[index].Substring(matchStepLine.Length); var stepMarkText = matchStepLine.Value.ToLower(); string line; if (stepMarkText.EndsWith("begin]")) { stepBaseCount = 0; //重置为1; } line = $"<p class=\"step-para\"><span class=\"step-mark\">Step {++stepBaseCount}</span> {FormatLineContent(ref md, tail.Trim(new char[] { ' ', ' ' }))}</p>"; //不必考虑全角字符,因为之前格式化时会将全角格式化为半角并补全。 if (stepMarkText.EndsWith("end]") /*|| stepMarkText.EndsWith("e]")*/) stepBaseCount = 0;//归零。 sBuilder.Append(line); sBuilder.Append("\r\n"); continue; } #endregion 处理步骤标记 // ================================================================================== // 注意,不要把其它项目的处理放在表格的处理之后,因为会无效!!!!!!!!!!! #region 处理表格信息 TableLinesInfo tableLinesInfo = InWhitchTable(index, tableSourcesList); if (null == tableLinesInfo) { sBuilder.Append(lineStrings[index] + separatorText[0]); sBuilder.Append("\r\n"); continue; } if (tableLinesInfo.IsUsed) continue; sBuilder.Append(tableLinesInfo.TableHtmlText); tableLinesInfo.IsUsed = true; #endregion //★★★★★★注意:不能将其它文本的处理放在表格信息之后,因为可能不起作用。 // ================================================================================ } stepBaseCount = 0;//整个处理结束后,必须归零,以便下次使用。 return sBuilder.ToString(); } /// <summary> /// 取 Header 标题的层级索引值。 /// </summary> /// <param name="lI1">一级标题会使前面所有高于一级的标题索引重置,并使一级标题的索引值加1。</param> /// <param name="lI2">二级标题会使前面所有高于二级的标题索引重置,并使二级标题的索引值加1。</param> /// <param name="lI3">三级标题会使前面所有高于三级的标题索引重置,并使三级标题的索引值加1。</param> /// <param name="lI4">四级标题会使前面所有高于四级的标题索引重置,并使四级标题的索引值加1。</param> /// <param name="lI5">五级标题会使前面所有六级标题索引重置,并使五级标题的索引值加1。</param> /// <param name="lI6">六级标题只会使六级标题索引加1。</param> /// <returns></returns> private static string BuildHeaderIndexText(int lI1, int lI2, int lI3, int lI4, int lI5, int lI6) { if (lI1 <= 0) return ""; StringBuilder sb = new StringBuilder(); sb.Append(lI1); if (lI2 <= 0) return sb.ToString() + "."; sb.Append("." + lI2); if (lI3 <= 0) return sb.ToString() + "."; sb.Append("." + lI3); if (lI4 <= 0) return sb.ToString() + "."; sb.Append("." + lI4); if (lI5 <= 0) return sb.ToString() + "."; sb.Append("." + lI5); if (lI6 <= 0) return sb.ToString() + "."; sb.Append("." + lI6); return sb.ToString() + "."; } /// <summary> /// 取 Header 标题的层级。 /// </summary> /// <param name="lineText">源文本。</param> /// <param name="headerMark">传出 Header 标题的标志文本。</param> /// <param name="headerContent">传出 Header 标题的内容文本。</param> private static int HeaderLevel(string lineText, out string headerMark, out string headerContent) { if (string.IsNullOrWhiteSpace(lineText)) { headerMark = ""; headerContent = lineText; return 0; } Regex regex = new Regex("^ {0,3}[##]{1,6}"); var match = regex.Match(lineText); if (match != null && match.Success) { var count = 0; foreach (char c in match.Value) { if (c == '#' || c == '#') count++; } headerMark = match.Value; headerContent = lineText.Substring(match.Length); return count; } headerMark = ""; headerContent = lineText; return 0; } /// <summary> /// 是否二维文字表行。 /// </summary> /// <param name="lineText">源文本。</param> private static bool IsTableRow(string lineText) { if (string.IsNullOrEmpty(lineText)) return false; if (lineText.Contains("|") == false && lineText.Contains("|") == false) return false; if (lineText.StartsWith(" ") || lineText.StartsWith(" ") || lineText.StartsWith("\t")) return false; var trim = lineText.Trim(); if (trim.StartsWith("+ ") || trim.StartsWith("* ") || trim.StartsWith("- ")) return false; if (lineText.StartsWith(">") || lineText.StartsWith("》")) return false; return true; } /// <summary> /// 处理转义字符: /// \* /// \\ /// \` /// \_ /// \{ /// \} /// \[ /// \] /// \( /// \) /// \# /// \+ /// \- /// \. /// \! /// </summary> /// <param name="sourceText">源文本。</param> public static string ConvertChar(string sourceText) { sourceText = sourceText.Replace("\\*", "*").Replace("\\\\", "\\").Replace("\\`", "`") .Replace("\\_", "_").Replace("\\{", "{").Replace("\\}", "}").Replace("\\[", "[") .Replace("\\]", "]").Replace("\\(", "(").Replace("\\)", ")").Replace("\\#", "#") .Replace("\\+", "+").Replace("\\-", "-").Replace("\\.", ".").Replace("\\!", "!"); return sourceText; } /// <summary> /// 取二维文字表列对齐方式。 /// </summary> /// <param name="columnDefinitions">列定义对象集合。</param> /// <param name="index">索引值,指定哪一列。</param> private static ColumnAlignment GetColumnAlign(List<ColumnAlignment> columnDefinitions, int index) { if (index < 0 || index >= columnDefinitions.Count || columnDefinitions.Count == 0) return ColumnAlignment.LEFT; return columnDefinitions[index]; } /// <summary> /// /// </summary> /// <param name="index"></param> /// <param name="tableLinesInfos"></param> /// <returns></returns> private static TableLinesInfo InWhitchTable(int index, List<TableLinesInfo> tableLinesInfos) { foreach (TableLinesInfo t in tableLinesInfos) { if (index >= t.StartLineIndex && index <= t.EndLineIndex) return t; } return null; } public static bool IsColumnAlignmentDefinitionLine(string text) { if (string.IsNullOrEmpty(text)) return false; if (text.Contains("-") == false && text.Contains("-") == false) return false;//防止将空表行当作表的“列对齐定义行”。 foreach (var c in text) { if (c == '-' || c == '-' || c == '|' || c == '|' || c == ' ' || c == ' ' || c == '\t' || c == ':' || c == ':' || c == '^') continue;//^表示该列应自动顺序编号 return false; } return true; } public static bool IsNumber(char @char) { if ((@char >= '1' && @char <= '9') || @char == '0') return true; if ((@char >= '1' && @char <= '9') || @char == '0') return true; return false; } public static bool IsHorizontalLineText(string lineText) { if (string.IsNullOrEmpty(lineText)) return false; if (lineText.StartsWith("\t")) return false;//以tab开头的是代码块<code> foreach (char c in lineText) { if (c != ' ' && c != ' ' && c != '-' && c != '-' && c != '\t')//中间带tab符也算,但不能以tab符开头。 return false; } return true; } /// <summary> /// 判断此行文本是否只包含“|”、“=”等。这样的行是“表格格式化功能”自动添加的分割表标题与表头行的装饰行。 /// </summary> /// <param name="lineText">行有意思一。</param> public static bool IsTableCaptionSplittor(string lineText) { if (string.IsNullOrEmpty(lineText)) return false; foreach (char c in lineText) { if (c == '=' || c == '|' || c == '|' || c == '=') continue; return false; } return true; } /// <summary> /// 任务列表的状态。 /// </summary> public enum TaskListItemState { NotTaskListItem, UnStart, Precessing, Finished, Aborted } /// <summary> /// 取格式化后的 Markdown 文本。 /// </summary> /// <param name="srcMarkdownText">尚未格式化的 Markdown 文本。</param> /// <returns></returns> public static string FormatMarkdownText(string srcMarkdownText) { string splitter = "\n"; //复制过来的东西可能只有 \n var lines = srcMarkdownText.Split(new string[] { splitter }, StringSplitOptions.None);//注意:空行有用,引用和正常文本间的切换需要空行。 splitter = "\r\n"; var sb = new StringBuilder(); var previewHeaderLevel = 0; var orderListNumber = 0; for (int i = 0; i < lines.Length; i++) { var text = lines[i].Replace("\r", "");// .Replace("\n", ""); 这个不需要了。 #region 保留空行 var trimedText = text.TrimStart(new char[] { ' ', ' ', '\t' }); if (string.IsNullOrEmpty(trimedText))//保留空行。 { sb.Append(splitter); continue; } #endregion #region 代码块原样输出 if (text.StartsWith(" ") || text.StartsWith("\t")) { sb.Append(text); sb.Append(splitter); continue; } #endregion #region 格式化被嵌入到引用块中的标题 Match match2; if (IsHeaderInBlockQuote(text, out match2)) { if (match2 != null && match2.Success) { StringBuilder sbTitleAfterBlockQuoter = new StringBuilder(); foreach (char c in match2.Value) { if (c == '#' || c == '#') { sbTitleAfterBlockQuoter.Append('#'); } } text = sbTitleAfterBlockQuoter.ToString() + text.Substring(match2.Length); } } #endregion #region 格式化标题 if (IsHeaderLine(text)) { Regex reg = new Regex(@"^[ ]{0,3}[##]{1,6}"); var match = reg.Match(text); if (match != null && match.Success) { //防止出现后一个标题比前一个层级高2层以上的情况 //例如,前一个标题是一级,紧跟着的一个标题却是三级,这不合逻辑。 string titleHeader; string titleTail; var titleLevel = HeaderLevel(text, out titleHeader, out titleTail); if (titleLevel > previewHeaderLevel + 1) { titleLevel = previewHeaderLevel + 1; previewHeaderLevel = titleLevel; StringBuilder sbHeader = new StringBuilder(); for (int x = 0; x < titleLevel; x++) { sbHeader.Append("#"); } titleHeader = sbHeader.ToString(); sb.Append(titleHeader); sb.Append(titleTail); sb.Append(splitter); continue; } else { previewHeaderLevel = titleLevel; sb.Append(match.Value.Replace("#", "#")); sb.Append(text.Substring(match.Length)); sb.Append(splitter); continue; } } } #endregion #region 日期行 if (IsDateLine(text)) { sb.Append(FormatDateLine(text)); sb.Append(splitter); continue; } #endregion #region TODO 型特殊注释文本行 string newTodoCommentMark; string newTodoCommentTail; if (IsTodoCommentLine(text, out newTodoCommentMark, out newTodoCommentTail, "TODO")) { sb.Append(";TODO:" + newTodoCommentTail); sb.Append(splitter); continue; } if (IsTodoCommentLine(text, out newTodoCommentMark, out newTodoCommentTail, "DONE")) { sb.Append(";DONE:" + newTodoCommentTail); sb.Append(splitter); continue; } if (IsTodoCommentLine(text, out newTodoCommentMark, out newTodoCommentTail, "DOING")) { sb.Append(";DOING:" + newTodoCommentTail); sb.Append(splitter); continue; } #endregion #region [Menu] 型特殊注释文本行 string newMenuMarkTail; if (IsMenuMarkLine(text, out newMenuMarkTail)) { sb.Append(";[Menu]:" + newMenuMarkTail); sb.Append(splitter); continue; } #endregion #region 演示模式 型特殊注释文本行 string newPresentationTypeTail; if (IsPresentationTypeLine(text, out newPresentationTypeTail)) { sb.Append(";PM:" + newPresentationTypeTail); sb.Append(splitter); continue; } #endregion #region 与试题相关的行 if (MarkDownEditorBase.IsStartWithTestPaperKeyWord(text)) { //text = trimedText;//试题元素前的全角空格需要保留。 sb.Append(text); sb.Append(splitter); continue; } #endregion #region 备注,以分号或冒号开头 if (IsCommentLine(text)) { //不再要求顶格,保留空格。全格式化成半角空格。 var commentStartIndex = 1; var isCompileComment = false; for (int iComment = 0; iComment < text.Length; iComment++) { var c = text[iComment]; if (c == ' ') continue; else if (c == ' ') continue; else if (c == '\t') continue; else if (c == ';' || c == ';') { commentStartIndex = iComment + 1; break; } else if (c == ':' || c == ':') { commentStartIndex = iComment + 1; isCompileComment = true; break; } } if (isCompileComment) { text = ":" + text.Substring(commentStartIndex); } else { text = ";" + text.Substring(commentStartIndex); } sb.Append(text); sb.Append(splitter); continue; } #endregion #region 如果是在树型文字表中的备注行(:开头注释行) Regex regCommentInTreeTextTable = new Regex(@"^([!!][ ├│└]{0,}){1,}[::]+"); var matchCommentInTreeTextTable = regCommentInTreeTextTable.Match(text); if (matchCommentInTreeTextTable.Success) { var header = matchCommentInTreeTextTable.Value; if (header.EndsWith(":")) { header = header.Substring(0, header.Length - 1) + ":"; } text = header + text.Substring(matchCommentInTreeTextTable.Length); sb.Append(text); sb.Append(splitter); continue; } #endregion #region 任务列表,必须顶格,否则易与Code块冲突 //形式是:[-][%][+][#],减号表示未开始,%号表示正在进行,加号表示已完成,[#]表示废弃。 //两个方括弧间可以有空格。 if (IsTaskLine(text)) { TaskListItemState state = TaskListItemState.UnStart; int index = text.IndexOf(']'); char[] trimArray = new char[] { ' ', '\t', ' ' }; var t1 = text.Substring(0, index + 1).Trim(trimArray); var t2 = text.Substring(index + 1);//跳过 ] foreach (char c in t1) { if (c == '[' || c == ']' || c == ' ' || c == '\t' || c == ' ') continue;//允许空格。 if (c == '-' || c == '-') { state = TaskListItemState.UnStart; break; } else if (c == '%') { state = TaskListItemState.Precessing; break; } else if (c == '+') { state = TaskListItemState.Finished; break; } else { state = TaskListItemState.NotTaskListItem; break; } } switch (state) { case TaskListItemState.UnStart: { text = "[-] " + t2.TrimStart(trimArray); break; } case TaskListItemState.Finished: { text = "[+] " + t2.TrimStart(trimArray); break; } case TaskListItemState.Precessing: { text = "[%] " + t2.TrimStart(trimArray); break; } case TaskListItemState.Aborted: { text = "[#] " + t2.TrimStart(trimArray); break; } default: { break;//非任务列表项,直接返回原文本。 } } sb.Append(text); sb.Append(splitter); continue; } #endregion #region 文档总标题,总是顶格 var htmlDocumentTitle = GetDocumentTitle(text); if (htmlDocumentTitle != null) { text = text.Trim(' ', '\t', ' '); if (text.ToLower().StartsWith("title>")) { text = "%" + text.Substring(6); } else if (text.StartsWith("标题>")) { text = "%" + text.Substring(3); } sb.Append(text); sb.Append(splitter); continue; } #endregion #region 作者信息,总是顶格 if (text.StartsWith("@") || text.StartsWith("·")) { text = text.TrimStart(' ', '\t', ' '); text = "@" + text.Substring(1).Trim(); sb.Append(text); sb.Append(splitter); continue; } #endregion #region 如果是水平线,总是顶格 if (IsHorizontalLineText(text)) { text = text.Replace(" ", "").Replace("\t", "").Replace("-", "-"); sb.Append(text); sb.Append(splitter); continue; } #endregion #region 格式化块引用 Regex regexBlockQuoter = new Regex(@"^[ ]{0,3}([>》〉]{1}[ ]{0,}){1,}"); var matchBlockQuoter = regexBlockQuoter.Match(text); if (matchBlockQuoter != null && matchBlockQuoter.Success) { text = matchBlockQuoter.Value.Replace(" ", "").Replace(" ", "").Replace("》", ">").Replace("〉", ">").Replace(">", "> ") + text.Substring(matchBlockQuoter.Length); sb.Append(text); sb.Append(splitter); continue; } #endregion #region 无序列表 if (text.StartsWith("-") || text.StartsWith("-") || text.StartsWith("+")) { //无序列表 int index = -1; for (int j = 0; j < text.Length; j++) { char c = text[j]; if (c != ' ' && c != ' ' && c != '-' && c != '-' && c != '+' && c != '*') { index = j; break; } } if (index >= 0) { if (index >= text.Length) text = "";//不允许空无序列表存在 else { text = text.Substring(0, index).Replace("-", "+").Replace("-", "+").Replace(" ", "").Replace(" ", "").Replace("*", "+").Replace("+", "+ ") + text.Substring(index); } } else { if (IsHorizontalLineText(text) == false)//以-开头还可能是水平线,不能去除 { text = ""; } else { text = text.Replace("\t", "").Replace("-", "-").Replace(" ", "").Replace(" ", ""); } } } else if (text.StartsWith("* ") || text.StartsWith("* ") || text.StartsWith("*\t")) { //*号开头的比较复杂,因为会和加粗、倾斜冲突——所以必须在星号后跟一个空格才算。 //仍然是无序列表 var anotherIndexOfStar = text.IndexOf("*", 2); if (anotherIndexOfStar < 0) { //如果此行中除开头外另存在一个星号,说明这应当作一个倾斜或加粗效果而不是无序列表。 int index = -1; for (int j = 0; j < text.Length; j++) { char c = text[j]; if (c != ' ' && c != ' ' && c != '-' && c != '-' && c != '+' && c != '*') { index = j; break; } } if (index >= 0) { if (index >= text.Length) text = "";//不允许空无序列表存在 else { text = text.Substring(0, index).Replace("-", "+").Replace("-", "+").Replace(" ", "").Replace(" ", "").Replace("*", "+").Replace("+", "+ ") + text.Substring(index); } } else { text = ""; } } else { //解决星号开头的无序列表与倾斜效果的冲突 //加粗是连续两个星号,不会被MarkdownSharp视为无序列表,不必考虑 text = text.Substring(0, anotherIndexOfStar) + "</em>" + text.Substring(anotherIndexOfStar + 1); text = "<em>" + text.Substring(1); } }//else... 否则只是加粗或倾斜。一行文本开头就加粗或倾斜是完全有可能的。 #endregion #region 有序列表 Regex regexOrderedList = new Regex(@"^[ ]{0,3}[01234567890123456789]{1,}\.[ \t]*"); var matchOrderedList = regexOrderedList.Match(text); if (matchOrderedList.Success) { text = $"{++orderListNumber}. " + text.Substring(matchOrderedList.Length); sb.Append(text); sb.Append(splitter); continue; } else { if (string.IsNullOrWhiteSpace(text) == false) orderListNumber = 0; } #endregion #region 添加了单独的“表格格式化”功能,此处不需要再格式化,否则反而会冲突。 //text = text.Replace("|", "|"); //if (text.Contains("|") || text.Contains("|")) //{ // var lineContent = text.Trim(new char[] { ' ', ' ', '\t' }); // if (lineContent.StartsWith("|") == false && lineContent.StartsWith("|") == false) // { // text = "|" + lineContent; // } // if (lineContent.EndsWith("|") == false && lineContent.EndsWith("|") == false) // { // text += "|"; // } //} //if (IsColumnDefinitionLine(text)) //{ // text = text.Replace(":", ":").Replace("-", "-"); //} #endregion #region 格式化自定义折叠区域 Regex regexStart = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?([!?!?IiWwEeQqIiWwEeQq][ \t]*)?\{"); var startMatch = regexStart.Match(text); if (startMatch.Success && text.Substring(startMatch.Length).IndexOf('}') < 0) { text = FormatRegionStartHeader(text); sb.Append(text); sb.Append(splitter); continue; } else { Regex regexEnd = new Regex(@"^[ ]{0,3}\}[ \t]*([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?"); var matchEnd = regexEnd.Match(text); if (matchEnd != null && matchEnd.Success) { text = "} " + text.Substring(matchEnd.Length); sb.Append(text); sb.Append(splitter); continue; } } #endregion #region 格式化 region 分割线 if (IsRegionSplitter(text)) { Regex regRegionSplitterStart = new Regex(@"^[ ]{0,3}[.。]{3,}[ \t]{0,}[rrRR][eeEE][ggGG][iiII][ooOO][nnNN]"); var matchRegionSplitterStart = regRegionSplitterStart.Match(text); if (matchRegionSplitterStart != null && matchRegionSplitterStart.Success) { sb.Append("... region "); var rSplitterContent = text.Substring(matchRegionSplitterStart.Length); Regex regRegionSplitterEnd = new Regex(@"[.。]{3,}[ \t]{0,}$"); var matchRegionSplitterEnd = regRegionSplitterEnd.Match(rSplitterContent); if (matchRegionSplitterEnd != null && matchRegionSplitterEnd.Success) { rSplitterContent = rSplitterContent.Substring(0, matchRegionSplitterEnd.Index); } sb.Append(rSplitterContent.Trim(new char[] { ' ', ' ', '\t' })); sb.Append(" ..."); sb.Append(splitter); continue; } } #endregion #region 格式化放大显示文本行 //用一对尖括号括起数字字符开头的行表示该行文本应放大尖括号中数字指定的倍数。 //注意:放大的倍数是应有限制。 Regex regexMuiltyText = new Regex(@"^[ ]{0,3}<[12345678901234567890]{1,}([.。][12345678901234567890]{1,}){0,}>"); var matchMuiltyText = regexMuiltyText.Match(text); if (matchMuiltyText.Success) { var tail = text.Substring(matchMuiltyText.Length); var header = matchMuiltyText.Value.Replace("1", "1").Replace("2", "2").Replace("3", "3") .Replace("4", "4").Replace("5", "5").Replace("6", "6").Replace("7", "7") .Replace("8", "8").Replace("9", "9").Replace("0", "0").Replace("。", ".") .Trim(new char[] { ' ', ' ' }); sb.Append(header); sb.Append(tail); sb.Append(splitter); continue; } #endregion #region 格式化步骤标记文本 Regex regexStepLine = new Regex(@"^[ ]{0,3}\[[ ]{0,}[sSsS]([tTtT][eEeE][pPpP]){0,1}[ ]{0,}(([bBbB]([eEeE][gGgG][iIiI][nNnN]){0,1})|([eEeE]([nNnN][dDdD]){0,1})){0,1}\]"); var matchStepText = regexStepLine.Match(text); if (matchStepText.Success) { var tail = text.Substring(matchStepText.Length); var header = matchStepText.Value.ToLower(); Regex endStep = new Regex(@"[eEeE]([nNnN][dDdD]){0,1}\]"); if (endStep.Match(header).Success) { sb.Append("[Step End] "); } else { Regex startStep = new Regex(@"[bBbB]([eEeE][gGgG][iIiI][nNnN]){0,1}\]"); if (startStep.Match(header).Success) { sb.Append("[Step Begin] "); } else sb.Append("[Step] "); } sb.Append(tail.Trim(new char[] { ' ', ' ' })); sb.Append(splitter); continue; } #endregion sb.Append(text); sb.Append(splitter); } return sb.ToString(); } /// <summary> /// 指定文本行是否引用块行。 /// </summary> public static bool IsBlockQuoteLine(string lineText) { Regex regexBlockQuoter = new Regex(@"^[ ]{0,3}([>》〉]{1}[ ]{0,}){1,}.*$"); var matchBlockQuoter = regexBlockQuoter.Match(lineText); if (matchBlockQuoter != null && matchBlockQuoter.Success) { return true; } return false; } /// <summary> /// 格式化自定义折叠区的标头文本。 /// </summary> /// <param name="lineText">源文本。</param> private static string FormatRegionStartHeader(string lineText) { if (string.IsNullOrEmpty(lineText)) return ""; Regex regexStartI = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?([IiIi][ \t]{0,})\{[ \t]{0,}"); var matchStartI = regexStartI.Match(lineText); if (matchStartI != null && matchStartI.Success) { return "I { " + lineText.Substring(matchStartI.Length); } Regex regexStartQ = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?([qQqQ??][ \t]{0,})\{[ \t]{0,}"); var matchStartQ = regexStartQ.Match(lineText); if (matchStartQ != null && matchStartQ.Success) { return "? { " + lineText.Substring(matchStartQ.Length); } Regex regexStartW = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?([wWwW!!][ \t]{0,})\{[ \t]{0,}"); var matchStartW = regexStartW.Match(lineText); if (matchStartW != null && matchStartW.Success) { return "! { " + lineText.Substring(matchStartW.Length); } Regex regexStartE = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?([eeEE][ \t]{0,})\{[ \t]{0,}"); var matchStartE = regexStartE.Match(lineText); if (matchStartE != null && matchStartE.Success) { return "E { " + lineText.Substring(matchStartE.Length); } Regex regexStart = new Regex(@"^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]{0,})?\{[ \t]{0,}"); var matchStart = regexStart.Match(lineText); if (matchStart != null && matchStart.Success) { return "{ " + lineText.Substring(matchStart.Length); } return lineText; } /// <summary> /// 是否单独占一行的图像链接文本行。 /// </summary> public static bool IsImageLinkLine(string text) { Regex regex = new Regex(@"^[ ]{0,3}!\[.*\]\(.*\).*$"); var match = regex.Match(text); return (match != null && match.Success); } /// <summary> /// 是否单独占一行的文件链接文本行。 /// </summary> public static bool IsFileLinkLine(string text) { Regex regex = new Regex(@"^\[.*\]\(.*\).*$"); var match = regex.Match(text); return (match != null && match.Success); } public static bool IsTreeListTextLine(string text) { Regex rgx = new Regex(@"^[!!][│ ]{0,}[├└]{0,}(?!([ \t]*\{)).*$"); var match = rgx.Match(text); return (match.Success && IsImageLinkLine(text) == false); } /// <summary> /// 这个变量用于解决“步骤”的计数。当遇到 step end 标签的时候,此值归零。 /// </summary> private static int stepBaseCount = 0; /// <summary> /// 在编译之前,移除一些不必编译到html文件中的行。这些行包括: /// ⑴文档总标题行——以%(或%)开头——即全、半角的百分号; /// ★这个比较特殊,只有第一个以%(或%)开头的会被当作标题,其它以%(或%)开头的行会原样参加编译。 /// ⑵注释行——以;(或;)开头——即全半角分号; /// ☆所有以;(或;)开头的行都会被忽略。 /// ⑶文档页眉——以~开头。 /// </summary> public static string RemoveExtraLines(string srcText, out string htmlDocumentTitle, out string htmlDocumentPageHeader, out string footerText, out bool compilePageMenu, out string headStyleTexts, out string bodyEndScripts) { //Script 如果不处理,会被 MarkdownSharp 当成普通段落文本。 String scripts; srcText = GetPageScripts(srcText, out scripts); bodyEndScripts = scripts; String styles; srcText = GetPageStyles(srcText, out styles); headStyleTexts = styles; //如果首行是任务列表(用来表示文档完成度),则移除首行。 //2017年1月7日 var regexOfLineEnd = new Regex(@"[\r\n]{1,}"); var matchOfEnd = regexOfLineEnd.Match(srcText); if (matchOfEnd.Success && matchOfEnd.Index > 0) { var fstLine = srcText.Substring(0, matchOfEnd.Length); if (IsTaskLine(fstLine)) { srcText = srcText.Substring(matchOfEnd.Index + matchOfEnd.Length); } } htmlDocumentPageHeader = htmlDocumentTitle = footerText = ""; compilePageMenu = false; if (string.IsNullOrEmpty(srcText)) { return string.Empty; } string[] lines; //尝试一下找“类型”,对填空题作些处理。 //之前在做HistoryAssist的填空题时,一些格式与Markdown不兼容,这里处理一下就能用了。 var startIndexOfType = srcText.IndexOf("类型>>"); var endIndexOfType = srcText.IndexOf("<<类型"); string typeText = ""; if (startIndexOfType >= 0 && endIndexOfType >= 0 && endIndexOfType > startIndexOfType) { typeText = srcText.Substring(startIndexOfType + 4, endIndexOfType - startIndexOfType - 4); } if (typeText == "填空" || typeText == "填空题") { srcText = srcText.Replace("【", "`").Replace("】", "`") .Replace("\n//", "\n;").Replace("\r//", "\n;"); } lines = CustomMarkdownSupport.FormatMarkdownText(srcText).Replace("\r\n", "\n") .Split(new char[] { '\r', '\n' }, StringSplitOptions.None);//要保留空行 #region 以惊叹号开头的行被视为树型文字表行,树型文字表以代码方式显示 //要防止单独占一行的图像链接文本和以惊叹号开头的自定义折叠区。 //注意:这个格式化不放在 FormatMarkdown()方法中。因为编译时才需要格式化,编辑时不需要格式化。 for (int x = 0; x < lines.Length; x++) { if (IsTreeListTextLine(lines[x])) { //if (IsImageLinkLine(lines[x])) continue; lines[x] = " " + lines[x]; } } #endregion StringBuilder sb = new StringBuilder(); bool removedDocumentTitleLine = false; bool removeFooterText = false; bool removePageHeader = false; //char[] trimChars = new char[] { '\t', ' ', ' ' }; //书写时文档标题、备注都必须严格的在行首添加标记字符——否则易与code块冲突。 //实际编译时执行“三个空格以内皆有效”。 var startLineIndex = 0; if (lines.Length > 0) { var lineFst = lines[0]; if (IsTaskLine(lineFst) || IsDateLine(lineFst)) { //文档完成状态的标记行,不编译进Html。 startLineIndex = 1;//忽略第1行 } } for (int i = startLineIndex; i < lines.Length; i++) { string line = lines[i]; if (removedDocumentTitleLine == false) { var title = GetDocumentTitle(line); if (title != null) { htmlDocumentTitle = title; removedDocumentTitleLine = true;//只有第一个以%开头的行才会被当作文档标题。 sb.Append("\n"); continue; } } if (IsPageHeader(line)) { if (removePageHeader == false) { htmlDocumentPageHeader = line.Substring(1).Trim(); removePageHeader = true;//只有第一个以~开头的行才会被当作页眉。 sb.Append("\n"); continue; } } else if (IsPageFoot(line))//即PageFooter { if (removeFooterText == false) { footerText = line.Substring(1); removeFooterText = true; sb.Append("\n"); continue; } } else if (IsCommentLine(line) && IsCompileCommentLine(line) == false) { if (IsMenuMarkText(line)) { compilePageMenu = true; } sb.Append("\n"); continue; //以;(或;)号开头的行是注释行,不会被编译进html文档。 } //任务列表原样输出!不再作为备注!2016年2月5日 //else if (IsTaskLine(line)) //{ // sb.Append("\n"); // continue; // //以“[-]、[%]、[+]”开头的行也是注释,但是比较特殊,属于“任务列表”。 //} else if (IsTableCaptionSplittor(line)) { //sb.Append("\n");//这个必须去除,否则表格的标题会与表格体分离——标题会成为一个单独的表格(只有一个单元格) continue; } //下面这些也应去除。 //<<<信息> // 标题>>明清君主专制制度的加强<<标题 // 日期>>2013年4月14日<<日期 // 作者>>杨震宇<<作者 // 电邮>>historyassist@163.com<<电邮 // 备注>>必修I,第一专题,第四节<<备注 //<信息>>> else if (line.StartsWith("<<<信息>")) { sb.Append("\n"); continue; } else if (line.StartsWith(" 类型>>")) { sb.Append("\n"); continue; } else if (line.StartsWith(" 标题>>")) { sb.Append("\n"); continue; } else if (line.StartsWith(" 日期>>")) { sb.Append("\n"); continue; } else if (line.StartsWith(" 作者>>")) { sb.Append("\n"); continue; } else if (line.StartsWith(" 辑录>>")) { sb.Append("\n"); continue; } else if (line.StartsWith(" 电邮>>")) { sb.Append("\n"); continue; } else if (line.StartsWith(" 备注>>")) { sb.Append("\n"); continue; } else if (line.StartsWith(" 电邮>>")) { sb.Append("\n"); continue; } else if (line.StartsWith("<信息>>>")) { sb.Append("\n"); continue; } else { //处理锚 //改造一下Markdown语法, //按Markdown原始语法,[链接名](http://www.xxx.com)表示一个链接。 //但我这里的“锚”,会是这个样子[锚名](@锚ID 锚说明文本) //锚名可以为空,锚ID必须以#号开头——否则它就只是一个普通的链接。 //锚ID不能包含空格,且最好是纯半角英文字母组成的。 //一个文档内的锚ID不能重复。 var text = line; if (text.StartsWith(" ") || text.StartsWith("\t")) { sb.Append(line); sb.Append("\n"); continue; } if (text.Contains("[") == false || text.Contains("]") == false) { sb.Append(line); sb.Append("\n"); continue; } if (text.Contains("(") == false && text.Contains("(") == false) { sb.Append(line); sb.Append("\n"); continue; } if (text.Contains(")") == false && text.Contains(")") == false) { sb.Append(line); sb.Append("\n"); continue; } text = text.Replace("[", "[[^]][") .Replace("(", "(").Replace(")", ")") .Replace(")", ")[[^]]").Replace(":", ":"); var spans = text.Split(new string[] { "[[^]]" }, StringSplitOptions.RemoveEmptyEntries); var sb2 = new StringBuilder(); foreach (var s in spans) { var index = s.IndexOf("](@"); if (s.StartsWith("[") && s.EndsWith(")") && index > 0) { //此片段是一个锚 //例如:[abc](@abcde) //abc是锚名,可以省略; //abcde是锚的ID,不可省略。 var anchorName = s.Substring(1, index - 1); var anchorID = s.Substring(index + 3, s.Length - index - 4); var pieces = anchorID.Split(new char[] { ' ', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (pieces.Length > 0) { anchorID = pieces[0];//其后文本均算锚的说明文本 //锚ID不应使用中文,但锚的说明可以。 } if (string.IsNullOrEmpty(anchorID)) { sb2.Append(s); } else { //当锚ID不为空时,才算是个锚,才需要编译 //锚的名称可以为空。 //如果锚名为空,浏览编译后的html文档时, //浏览器不会呈现这个锚——但是对这个锚的链接仍然有效。 sb2.Append("<span id=\"" + anchorID + "\" class=\"anchor\">" + anchorName + "</span>"); } } else sb2.Append(s); } line = sb2.ToString(); } sb.Append(line); sb.Append("\n"); } //尝试再找下标题 if (string.IsNullOrEmpty(htmlDocumentTitle)) { var startIndex = srcText.IndexOf("标题>>"); var endIndex = srcText.IndexOf("<<标题"); if (startIndex >= 0 && endIndex >= 0 && endIndex > startIndex) { htmlDocumentTitle = srcText.Substring(startIndex + 4, endIndex - startIndex - 4); } } if (string.IsNullOrEmpty(footerText)) { //脚注一般用于显示作者 var startIndex = srcText.IndexOf("作者>>"); var endIndex = srcText.IndexOf("<<作者"); if (startIndex >= 0 && endIndex >= 0 && endIndex > startIndex) { footerText = srcText.Substring(startIndex + 4, endIndex - startIndex - 4); } if (string.IsNullOrEmpty(footerText)) { startIndex = srcText.IndexOf("辑录>>"); endIndex = srcText.IndexOf("<<辑录"); if (startIndex >= 0 && endIndex >= 0 && endIndex > startIndex) { footerText = srcText.Substring(startIndex + 4, endIndex - startIndex - 4); } } } return sb.ToString().Replace("\r", ""); } /// <summary> /// 是否任务列表项文本。 /// </summary> /// <param name="lineText">源文本。</param> public static bool IsTaskLine(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; Regex regex = new Regex(@"^ {0,3}\[[ \t]*[\-\+\%\#-+%#][ \t]*\][^\(].*$"); var match = regex.Match(lineText); return match.Success; } /// <summary> /// 是否“废弃”任务列表项。 /// </summary> /// <param name="lineText">源文本。</param> public static bool IsAbortedTaskLine(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; Regex regex = new Regex(@"^ {0,3}\[[ \t]*[\##][ \t]*\][^\(].*$"); var match = regex.Match(lineText); return match.Success; } /// <summary> /// 是否以分号开头的注释文本。 /// </summary> /// <param name="lineText">源文本。</param> public static bool IsCommentLine(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; if (lineText.StartsWith(" ") || lineText.StartsWith("\t")) return false;//这是代码块 var tmp = lineText.TrimStart(new char[] { ' ', '\t', ' ' }); if (tmp.StartsWith(";") || tmp.StartsWith(";") || tmp.StartsWith(":") || tmp.StartsWith(":")) return true; return false; } /// <summary> /// 是否指示“要编译 Html 菜单”的标记文本。 /// </summary> /// <param name="lineText">文本行。</param> public static bool IsMenuMarkText(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; if (lineText.StartsWith(" ") || lineText.StartsWith("\t")) return false;//这是代码块 Regex r = new Regex(@"^[ ]{0,3}[;;](\[?(([mMmM][eEeE][nNnN][uUuU])|(菜单))\]?)[::].*$"); var match = r.Match(lineText); if (match != null && match.Success) { return true; } return false; } /// <summary> /// 是否时间标签文本。 /// </summary> /// <param name="lineText">源文本。</param> public static bool IsDateLine(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; if (lineText.StartsWith(" ") || lineText.StartsWith("\t")) return false;//这是代码块 //Regex r = new Regex(@"^[ ]{0,3}\[((((1[6-9]|[2-9]\d)\d{2})[-.,,。-、./年](0?[13578]|1[02])[-.,,。-、./月](0?[1-9]|[12]\d|3[01])日{0,1})|(((1[6-9]|[2-9]\d)\d{2})[-.,,。-、./年](0?[13456789]|1[012])[-.,,。-、./月](0?[1-9]|[12]\d|30)日{0,1})|(((1[6-9]|[2-9]\d)\d{2})[-.,,。-、./年]0?2[-.,,。-、./月](0?[1-9]|1\d|2[0-8])日{0,1})|(((1[6-9]|[2-9]\d)(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00))[-.,,。-、./年]0?2-29[-.,,。-、./月])日{0,1})\]"); Regex r = new Regex(@"^[ ]{0,3}\[\d{4,4}[-.,,。-、./年][01]{0,1}\d[-.,,。-、./月][0123]{0,1}\d日{0,1}([ ]{1,}\d{1,2}[::]\d{1,2}){0,1}\](\[[ \t]*[\-\%\+\#-+%#][ \t]*\]){0,1}"); var match = r.Match(lineText); if (match != null && match.Success) { var result = GetDateFromDateLine(lineText); if (result == null) return false; if (result.HasValue == false) return false; return true; } return false; } /// <summary> /// 格式化日期标签文本行的日期标志文本部分。 /// </summary> /// <param name="dateMarkText">日期标志文本。形如:[2016-2-1]之类。</param> public static string FormatDateLineMark(string dateMarkText) { var trimChars = new char[] { ' ', ' ', '\t' }; return dateMarkText.Replace("/", "-").Replace(".", "-").Replace(",", "-").Replace(",", "-") .Replace("。", "-").Replace("-", "-").Replace("、", "-").Replace(".", "-") .Replace("年", "-").Replace("月", "-").Replace("日", "").Replace("-", "-").Replace(":", ":").Trim(trimChars); } /// <summary> /// 格式化日期文本行。 /// </summary> /// <param name="dateLineText">要格式化的日期文本行。</param> public static string FormatDateLine(string dateLineText) { if (string.IsNullOrWhiteSpace(dateLineText)) return ""; var replacedLine = dateLineText.Replace("-", "-"); var index = replacedLine.IndexOf("]"); if (index < 0) return dateLineText;//原样返回 string header; string tail; if (replacedLine.Length >= index + 2) { char c = replacedLine[index + 1]; if (c == '-' || c == '-' || c == '%' || c == '#' || c == '+') { header = dateLineText.Substring(0, index + 1) + c; tail = dateLineText.Substring(index + 2); } else { header = dateLineText.Substring(0, index + 1); tail = dateLineText.Substring(index + 1); } } else { header = dateLineText.Substring(0, index + 1); tail = dateLineText.Substring(index + 1); } var trimChars = new char[] { ' ', ' ', '\t' }; //-.,,。-、./ var secHeaderMark = ""; var tailTrim = tail.Trim(trimChars); if (tail.StartsWith("[-]") || tail.StartsWith("[-]") || tail.StartsWith("[%]") || tail.StartsWith("[#]") || tail.StartsWith("[+]")) { secHeaderMark = tail.Substring(0, 3).Replace("[-]", "[-]"); tail = tailTrim.Substring(3); } return " " + FormatDateLineMark(header) + secHeaderMark + " " + tail.Trim(trimChars); } /// <summary> /// 从日期文本行中取日期标志文本部分。 /// </summary> /// <param name="dateLine">日期文本行。</param> public static DateTime? GetDateFromDateLine(string dateLine) { if (string.IsNullOrWhiteSpace(dateLine)) return null; var index = dateLine.IndexOf("]"); DateTime dt; if (index < 0) { if (DateTime.TryParse(dateLine, out dt)) return dt; return null;//原样返回 } var header = dateLine.Substring(0, index + 1).Replace("[", "").Replace("]", ""); if (DateTime.TryParse(header, out dt)) return dt; return null; } /// <summary> /// 取时间文本中的任务完成状态标记文本。 /// </summary> /// <param name="dateLine">时间文本。</param> public static string GetDateLineStatusMark(string dateLine) { if (string.IsNullOrWhiteSpace(dateLine)) return ""; Regex regex = new Regex(@"\]\[[ \t]*[--+#%][ \t]*\]"); var match = regex.Match(dateLine); if (match != null && match.Success) { return dateLine.Substring(match.Index + 1, match.Length - 1).Replace(" ", "").Replace(" ", "").Replace("\t", ""); } return ""; } /// <summary> /// 取时间文本行中的内容文本(表示该时间标签指代的时间发生的事件的具体意义,而非时间标签所指代的时间)。 /// </summary> /// <param name="dateLine">时间文本。</param> public static string GetContentTextOfDateLine(string dateLine) { if (string.IsNullOrWhiteSpace(dateLine)) return ""; Regex regex = new Regex(@"\[[ \t]{0,}[--%#+][ \t]{0,}\][ \t].*$"); var match = regex.Match(dateLine); if (match != null && match.Success) { var index = match.Value.IndexOf("]"); if (index < 0) return match.Value; return match.Value.Substring(index + 1).Trim(new char[] { ' ', ' ', '\t' }); } return ""; } /// <summary> /// 是否以冒号开头的注释。这种注释会被编译进 Html 文档,并在 Html 页面上呈现特殊效果。 /// </summary> /// <param name="lineText">源文本。</param> public static bool IsCompileCommentLine(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; if (lineText.StartsWith(" ") || lineText.StartsWith("\t")) return false;//这是代码块 var tmp = lineText.TrimStart(new char[] { ' ', '\t', ' ' }); if (tmp.StartsWith(":") || tmp.StartsWith(":")) return true; return false; } /// <summary> /// 取文档标题文本。 /// </summary> /// <param name="lineText">以 % 开头的文本行。</param> /// <returns>返回去除 % 标记的其它文本。</returns> public static string GetDocumentTitle(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return null; var text = lineText.Replace(" ", " "); if (text.StartsWith(" ") || text.StartsWith("\t")) return null; //注意:[空格][Tab]不会被编译为Code行。 var tmp = lineText.TrimStart(new char[] { ' ', '\t', ' ' }); if (lineText.StartsWith("%")) return tmp.Substring(1); var tmp2 = tmp.Replace("T", "t").Replace("t", "t").Replace("I", "i").Replace("i", "i").Replace("L", "l").Replace("l", "l").Replace("E", "e").Replace("e", "e") .Replace("〉", ">").ToLower(); if (tmp2.StartsWith("title>")) return tmp.Substring(6); if (tmp.StartsWith("标题>") || tmp.StartsWith("标题〉")) return tmp.Substring(3); return null; } /// <summary> /// 判断文本行是否页脚文本。(页脚文本以 @ 或 · 开头。) /// </summary> /// <param name="lineText">源文本。</param> /// <returns></returns> public static bool IsPageFoot(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; var text = lineText.Replace(" ", " "); if (text.StartsWith(" ") || text.StartsWith("\t")) return false; //注意:[空格][Tab]不会被编译为Code行。 var tmp = lineText.TrimStart(new char[] { ' ', '\t', ' ' }); if (tmp.StartsWith("@") || tmp.StartsWith("·")) return true; return false; } /// <summary> /// 判断文本行是否页眉文本。(页眉文本以波形符~开头。) /// </summary> /// <param name="lineText">源文本。</param> /// <returns>返回去除波形符以后的其余文本。</returns> public static bool IsPageHeader(string lineText) { if (string.IsNullOrWhiteSpace(lineText)) return false; var text = lineText.Replace(" ", " "); if (text.StartsWith(" ") || text.StartsWith("\t")) return false; //注意:[空格][Tab]不会被编译为Code行。 var tmp = lineText.TrimStart(new char[] { ' ', '\t', ' ' }); if (tmp.StartsWith("~")) return true; return false; } /// <summary> /// 判断路径指向的 Markdown 文档是否被加密。 /// </summary> /// <param name="fullFilePath">Markdown 文件路径。</param> private static bool IsEncripied(string fullFilePath) { if (String.IsNullOrWhiteSpace(fullFilePath) || File.Exists(fullFilePath) == false) return false; System.IO.StreamReader s = File.OpenText(fullFilePath); var fstEncriptLine = s.ReadLine(); if (string.IsNullOrEmpty(fstEncriptLine)) return false; var fstDecriptedLine = SetPasswordPanel.TextDecrypt(fstEncriptLine.Replace("[[<r>]]", "\r").Replace("[[<n>]]", "\n").ToCharArray(), "DyBj#PpBb"); if (fstDecriptedLine.Contains("Password:") && fstDecriptedLine.Contains("Question:") && fstDecriptedLine.Contains("|")) { return true; } return false; } /// <summary> /// 用于决定按何种方式将 Markdown 切分为幻灯片。 /// </summary> public enum PresentateHtmlSplitterType { /// <summary> /// 只演示自定义折叠区中的内容,其它忽略。 /// </summary> Region, /// <summary> /// 演示所有一级标题下属的内容,但不包括第一个一级标题之前的内容。 /// 这样设计的原因在于:第一个一级标题前可能只有文档标题而没有其它内容——这时候会显示为一个空页面。 /// </summary> TopLevelHeader, /// <summary> /// 演示时按水平线分割。 /// </summary> HorizontalLine, /// <summary> /// 按照文档中定义的方式来,方式应为下列三种之一。 /// 如果文档中没有定义,则默认按一级标题(TopLevelHeader)。 /// </summary> ByDocument, /// <summary> /// 直接演示整个文档。与“调用外部浏览器预览”相比,唯一的不同只是使用不同的CSS,从而使得字体不同。 /// </summary> WholeDocument, /// <summary> /// 演示指定的文本片段。 /// </summary> TextSegment, } /// <summary> /// 将一个 Markdown 文件按水平线拆分成多个文件,再编译。 /// 这是为了演示。 /// </summary> /// <param name="srcFilePath"></param> /// <param name="htmlHeadersCollpase"></param> /// <param name="htmlTitle"></param> /// <returns></returns> public static List<CustomUri> Compile(string srcFilePath, MainWindow.HtmlHeadersCollapseType htmlHeadersCollpase, PresentateHtmlSplitterType presentateHtmlSplitterType, ref List<string> htmlFilesList, ref List<string> mdFilesList, bool onlyPresentateHeaders = false, string textSetment = null, string previewPageLink = "", string nextPageLink = "") { List<CustomUri> uris = new List<CustomUri>(); htmlFilesList.Clear(); mdFilesList.Clear(); if (File.Exists(srcFilePath) == false) return uris; var sourceText = File.ReadAllText(srcFilePath); switch (presentateHtmlSplitterType) { case PresentateHtmlSplitterType.Region: { SplitAndCompileByRegions(srcFilePath, sourceText, htmlHeadersCollpase, ref uris, ref htmlFilesList, ref mdFilesList); break; } case PresentateHtmlSplitterType.TopLevelHeader: { SplitAndCompileByTopLevelHeaders(srcFilePath, sourceText, htmlHeadersCollpase, ref uris, ref htmlFilesList, ref mdFilesList, onlyPresentateHeaders); break; } case PresentateHtmlSplitterType.WholeDocument: { CompileByWholeDocument(srcFilePath, sourceText, htmlHeadersCollpase, ref uris, ref htmlFilesList, ref mdFilesList); break; } case PresentateHtmlSplitterType.TextSegment: { CompileTextSegment(srcFilePath, sourceText, htmlHeadersCollpase, ref uris, ref htmlFilesList, ref mdFilesList, textSetment); break; } case PresentateHtmlSplitterType.HorizontalLine: default: { SplitAndCompileByHorizontalLine(srcFilePath, sourceText, htmlHeadersCollpase, ref uris, ref htmlFilesList, ref mdFilesList); break; } } return uris; } public static void CompileTextSegment(string srcFilePath, string sourceText, MainWindow.HtmlHeadersCollapseType htmlHeadersCollpase, ref List<CustomUri> uris, ref List<string> htmlFilesList, ref List<string> mdFilesList, string textSegment) { string htmlTitle = ""; var lines = File.ReadLines(srcFilePath); foreach (var line in lines) { var title = GetDocumentTitle(line); if (title != null) { htmlTitle = title; break; } } var newPathPrefix = ""; if (srcFilePath.ToLower().EndsWith(".md")) { newPathPrefix = srcFilePath.Substring(0, srcFilePath.Length - 3); } else newPathPrefix = srcFilePath; var pageFileName = newPathPrefix + "_tmp~.md"; File.WriteAllText(pageFileName, $"\r\n%{htmlTitle}\r\n\r\n{textSegment}"); var newHtmlFilePath = Compile(pageFileName, htmlHeadersCollpase, PresentateHtmlSplitterType.TextSegment, 1, ref htmlTitle, false, true, Globals.MainWindow.mainTabControl.FontSize / 16); if (string.IsNullOrWhiteSpace(newHtmlFilePath) == false) { mdFilesList.Add(pageFileName); htmlFilesList.Add(newHtmlFilePath); uris.Add(new CustomUri() { Uri = new Uri("file:///" + newHtmlFilePath), MarkdownTitle = htmlTitle, }); } } public static void CompileByWholeDocument(string srcFilePath, string sourceText, MainWindow.HtmlHeadersCollapseType htmlHeadersCollpase, ref List<CustomUri> uris, ref List<string> htmlFilesList, ref List<string> mdFilesList) { string htmlTitle = ""; var lines = File.ReadLines(srcFilePath); foreach (var line in lines) { var title = GetDocumentTitle(line); if (title != null) { htmlTitle = title; break; } } var newPathPrefix = ""; if (srcFilePath.ToLower().EndsWith(".md")) { newPathPrefix = srcFilePath.Substring(0, srcFilePath.Length - 3); } else newPathPrefix = srcFilePath; var pageFileName = newPathPrefix + "_tmp~.md"; File.WriteAllText(pageFileName, sourceText); var newHtmlFilePath = Compile(pageFileName, htmlHeadersCollpase, PresentateHtmlSplitterType.WholeDocument, 1, ref htmlTitle, false, true, 1.5f); if (string.IsNullOrWhiteSpace(newHtmlFilePath) == false) { mdFilesList.Add(pageFileName); htmlFilesList.Add(newHtmlFilePath); uris.Add(new CustomUri() { Uri = new Uri("file:///" + newHtmlFilePath), MarkdownTitle = htmlTitle, }); } } public static void SplitAndCompileByHorizontalLine(string srcFilePath, string sourceText, MainWindow.HtmlHeadersCollapseType htmlHeadersCollpase, ref List<CustomUri> uris, ref List<string> htmlFilesList, ref List<string> mdFilesList) { var newPathPrefix = ""; if (srcFilePath.ToLower().EndsWith(".md")) { newPathPrefix = srcFilePath.Substring(0, srcFilePath.Length - 3); } else newPathPrefix = srcFilePath; string htmlTitle = ""; var lines = File.ReadLines(srcFilePath); List<string> pageTexts = new List<string>(); StringBuilder sb = new StringBuilder(); foreach (var line in lines) { var title = GetDocumentTitle(line); if (title != null && htmlTitle == "") { htmlTitle = title; continue; } if (IsHorizontalLineText(line)) { pageTexts.Add(sb.ToString()); sb = new StringBuilder(); sb.Append("\r\n"); continue; } sb.Append(line); sb.Append("\r\n"); } pageTexts.Add(sb.ToString()); for (int i = 0; i < pageTexts.Count; i++) { var pageText = "%" + htmlTitle + "\r\n\r\n" + pageTexts[i]; var pageFileName = newPathPrefix + $"_{i + 1}_tmp~.md"; File.WriteAllText(pageFileName, pageText); var newHtmlFilePath = Compile(pageFileName, htmlHeadersCollpase, PresentateHtmlSplitterType.HorizontalLine, i + 1, ref htmlTitle, false, true, 1.5f); if (string.IsNullOrWhiteSpace(newHtmlFilePath) == false) { mdFilesList.Add(pageFileName); htmlFilesList.Add(newHtmlFilePath); uris.Add(new CustomUri() { Uri = new Uri("file:///" + newHtmlFilePath), MarkdownTitle = htmlTitle, }); } } } public static void SplitAndCompileByTopLevelHeaders(string srcFilePath, string sourceText, MainWindow.HtmlHeadersCollapseType htmlHeadersCollpase, ref List<CustomUri> uris, ref List<string> htmlFilesList, ref List<string> mdFilesList, bool onlyPresentateHeaders) { var newPathPrefix = ""; if (srcFilePath.ToLower().EndsWith(".md")) { newPathPrefix = srcFilePath.Substring(0, srcFilePath.Length - 3); } else newPathPrefix = srcFilePath; string htmlTitle = ""; var lines = File.ReadLines(srcFilePath); List<string> pageTexts = new List<string>(); StringBuilder sb = new StringBuilder(); foreach (var line in lines) { var title = GetDocumentTitle(line); if (title != null && htmlTitle == "") { htmlTitle = title; continue; } Regex regexTopLevelHeader = new Regex(@"^[ ]{0,3}[##][^##]*$"); var match = regexTopLevelHeader.Match(line); if (match != null && match.Success) { pageTexts.Add(sb.ToString()); sb = new StringBuilder(); sb.Append(line); sb.Append("\r\n"); continue; } if (onlyPresentateHeaders) { Regex regexHeader = new Regex(@"^[ ]{0,3}[##].*$"); var matchHeader = regexHeader.Match(line); if (matchHeader != null && matchHeader.Success) { sb.Append(line); sb.Append("\r\n"); } } else { sb.Append(line); sb.Append("\r\n"); } } pageTexts.Add(sb.ToString()); for (int i = 1; i < pageTexts.Count; i++)//从1开始,不显示第一个一级标题前面的内容。 { var pageText = "%" + htmlTitle + "\r\n\r\n" + pageTexts[i]; var pageFileName = newPathPrefix + $"_{i + 1}_tmp~.md"; File.WriteAllText(pageFileName, pageText); var newHtmlFilePath = Compile(pageFileName, htmlHeadersCollpase, PresentateHtmlSplitterType.TopLevelHeader, i, ref htmlTitle, false, true, 1.5f); if (string.IsNullOrWhiteSpace(newHtmlFilePath) == false) { mdFilesList.Add(pageFileName); htmlFilesList.Add(newHtmlFilePath); uris.Add(new CustomUri() { Uri = new Uri("file:///" + newHtmlFilePath), MarkdownTitle = htmlTitle, }); } } } public static void SplitAndCompileByRegions(string srcFilePath, string sourceText, MainWindow.HtmlHeadersCollapseType htmlHeadersCollpase, ref List<CustomUri> uris, ref List<string> htmlFilesList, ref List<string> mdFilesList) { //★注意:C# 正则表达式在多行查询时,要匹配包含换行符在内的所有字符,必须使用([\s\S]*)表示。 // 加上?是表示非贪婪模式。 Regex regionRegex = new Regex(@"(?<=(^[ ]{0,3}([rrRR][eeEE][ggGG][iiII][ooOO][nnNN][ \t]*)?([iweqiweq][ \t]*)?\{).*$)[\s\S]*?(?=(^[ ]{0,3}\}[ \t]*[rrRR][eeEE][ggGG][iiII][ooOO][nnNN].*$))", RegexOptions.Multiline); var matches = regionRegex.Matches(sourceText); if (matches == null || matches.Count <= 0) { return; } var newPathPrefix = ""; if (srcFilePath.ToLower().EndsWith(".md")) { newPathPrefix = srcFilePath.Substring(0, srcFilePath.Length - 3); } else newPathPrefix = srcFilePath; string htmlTitle = ""; var lines = File.ReadLines(srcFilePath); foreach (var line in lines) { var title = GetDocumentTitle(line); if (title != null) { htmlTitle = title; break; } } for (int i = 0; i < matches.Count; i++) { var pageText = "%" + htmlTitle + "\r\n\r\n" + matches[i].Value; var pageFileName = newPathPrefix + $"_{i + 1}_tmp~.md"; File.WriteAllText(pageFileName, pageText); var newHtmlFilePath = Compile(pageFileName, htmlHeadersCollpase, PresentateHtmlSplitterType.Region, i + 1, ref htmlTitle, false, true, 1.5f); if (string.IsNullOrWhiteSpace(newHtmlFilePath) == false) { mdFilesList.Add(pageFileName); htmlFilesList.Add(newHtmlFilePath); uris.Add(new CustomUri() { Uri = new Uri("file:///" + newHtmlFilePath), MarkdownTitle = htmlTitle, }); } } } /// <summary> /// 将 Markdown 文件编译为 Html 网页文件。基本流程如下: /// 1.对 Markdown 文件中的文本进行格式化; /// 2.移除不必要编译进 Html 中的行; /// 3.对 Markdown 进行预处理,将一些自定义的 Markdown 元素先编译为 Html 标签; /// 4.调用 MarkdownSharp 对经过预处理的 Markdown 文档进行编译; /// 5.对 MarkdownSharp 编译后的 Html 文本进行追加处理,使之支持六级标题等功能; /// 6.根据偏好设置决定是否对追加处理后的 Html 文本进行格式化。 /// </summary> /// <param name="srcFilePath">要编译的 Markdown 源文件的路径。</param> /// <param name="htmlHeadersCollpase">在编译好的 Html 文档中六级标题是否支持折叠功能。</param> /// <param name="presentateHtmlSplitterType">编译整个文档时应传入 null。</param> /// <param name="startH1Number">此参数用以指定<H1>的编号从什么值开始,通常应传入1。 /// 此参数是为了编译切块的幻灯片 Html 文档而存在的。</param> /// <param name="htmlTitle">编译好的 Html 文档应以什么文本作为标题。此标题应由 Markdown 文件内部设置的“%xxx”行决定。</param> /// <param name="withoutAdonors">不向页面添加标题、脚注、编译时间等元素。</param> /// <returns>如果编译成功,返回新文件路径。</returns> public static string Compile(string srcFilePath, MainWindow.HtmlHeadersCollapseType htmlHeadersCollpase, PresentateHtmlSplitterType? presentateHtmlSplitterType, int startH1Number, ref string htmlTitle, bool addBackToIndexLink, bool withoutAdonors = false, double webBrowserZoom = 1, string previewPageLink = "", string nextPageLink = "") { string shortFileName; int lastIndex = srcFilePath.LastIndexOf("\\"); if (lastIndex >= 0) { string name = srcFilePath.Substring(lastIndex + 1); if (string.IsNullOrEmpty(name)) { shortFileName = "unnamed.md"; } else shortFileName = name; } else { shortFileName = "unnamed.md"; } string htmlDocumentTitle; string htmlDocumentPageHeader; string footerText; MarkdownSharp.Markdown md = new MarkdownSharp.Markdown(); string contentText; bool isEncrypted = false; string password, passwordTip; isEncrypted = IsFileEncrypted(srcFilePath, out password, out passwordTip); if (isEncrypted) { string result = null; foreach (UIElement ue in Globals.MainWindow.mainTabControl.Items) { var edit = ue as MarkdownEditor; if (edit != null && string.IsNullOrEmpty(edit.FullFilePath) == false && edit.FullFilePath.ToLower() == srcFilePath.ToLower()) { if (edit.InputPasswordPanel.Visibility == Visibility.Collapsed) { result = edit.Password; } } } if (result == null) { result = InputBox.Show(Globals.AppName, $"{passwordTip}", "", false, "注意:密码区分大小写。", true); } if (result == null) { return ""; } else { if (result == password) { var allText = File.ReadAllText(srcFilePath); contentText = SetPasswordPanel.TextDecrypt(allText.Substring(allText.IndexOf("\r\n") + 2).ToCharArray(), "DyBj#PpBb"); } else { LMessageBox.Show("密码错误,无法编译此文件:\r\n" + srcFilePath, Globals.AppName, MessageBoxButton.OK, MessageBoxImage.Warning); if (srcFilePath.ToLower().EndsWith(".md")) { var htmlFilePath = srcFilePath.Substring(0, srcFilePath.Length - 3) + ".html"; if (File.Exists(htmlFilePath)) { try { File.Delete(htmlFilePath); } catch (Exception ex) { LMessageBox.Show("无法删除已存在的 Html 文件:\r\n" + htmlFilePath + "\r\n" + "错误信息如下:\r\n" + ex.Message, Globals.AppName, MessageBoxButton.OK, MessageBoxImage.Warning); return null; } } return null; } else return null; } } } else { contentText = File.ReadAllText(srcFilePath); } bool compilePageMenu; string headStyleTexts; //取出内部样式表文本。 //CSS 样式表分外部样式表(用Link链接)、内部样式表(放在 Head 部分)、内联样式表(Inline)。 //这里是指要放在 Html 页面的 Head 部分的样式表——它只作用于当前页面本身,由用户直接在文档中书写。 string bodyEndScripts; //取出页面内手工写的脚本。 //如果不提取, Markdown Sharp 会将这些脚本处理成普通段落文本,从而导致无效。 string htmlBody = RemoveExtraLines(contentText, out htmlDocumentTitle, out htmlDocumentPageHeader, out footerText, out compilePageMenu, out headStyleTexts, out bodyEndScripts); if (Globals.MainWindow.CompilePageMenu) compilePageMenu = true; //强制编译出页面菜单 if (withoutAdonors) compilePageMenu = false; //演示模式下,不添加此左栏菜单。 bool forbiddenAutoNumber = true; if (presentateHtmlSplitterType != null) { if (presentateHtmlSplitterType.Value == PresentateHtmlSplitterType.TopLevelHeader) { //当切分处理一个 Markdown 文件时,只有使用一级标题作为分割标准才能保证序号不出错。 //所以其它两种分割情况下,都必须 forbiddenAutoNumber = false; } } else forbiddenAutoNumber = false;//如果未传入这个值,说明不是在切分编译。 htmlBody = CustomMarkdownSupport.PreProcessor(htmlBody, startH1Number, forbiddenAutoNumber, new char[] { '\n' }); if (string.IsNullOrEmpty(htmlDocumentTitle)) { htmlDocumentTitle = "Markdown Document"; } //判断文件中是否还没有书写实际内容 bool emptyDoc = false; //初始默认值为false 是有道理的 if (string.IsNullOrWhiteSpace(htmlBody)) { emptyDoc = true; } else { emptyDoc = true; //这里必须改为 true for (int i = 0; i < htmlBody.Length; i++) { char c = htmlBody[i]; if (c == '\n' || c == ' ' || c == '\t' || c == ' ') continue; // 理论上,如果没有写内容,htmlBody只是会一连串的换行符(\n)组成的串。 emptyDoc = false; //只要找到一个有意义的字符,文档就不是“空”的。 break; } } if (emptyDoc) htmlBody = "(此文档尚未添加内容)"; htmlBody = md.Transform(htmlBody); string directoryLevelMark = GetDirectoryLevelMark(srcFilePath); var removeEmptyParagraphsRegex = new Regex(@"[ \t]*<p>[ \t]*</p>[ \t]*"); htmlBody = removeEmptyParagraphsRegex.Replace(htmlBody, ""); htmlBody = CustomMarkdownSupport.AppendProcessor(htmlBody, directoryLevelMark, htmlHeadersCollpase, Globals.MainWindow.CompileCodeToFillBlank, compilePageMenu, (addBackToIndexLink ? srcFilePath : null), previewPageLink, nextPageLink); //htmlBody = htmlBody.Replace("<p><div ", "<div ").Replace("</div></p>", "</div>");//去除不必要的空段。 if (string.IsNullOrWhiteSpace(htmlBody)) { return null; } //去除不必要的空段。 htmlBody = Regex.Replace(htmlBody, @"<[pP]>[ \t]*((<[iI][mM][gG].*/>)|(<[dD][iI][vV]>*.</[dD][iI][vV]>))[ \t]*</[pP]>", new MatchEvaluator(FormatParagraphsTag)); if (string.IsNullOrEmpty(htmlBody) == false) { // 能不能支持实时切换主题并使用 Cookie 来保存用户的选择呢? // 理论上当然是可以的,现成的实现方案也有——利用 jQuery.cookie.js 可以方便地读、写cookie。 // 但问题在于:1.实时切换主题必须要考虑填空题与主题配色的切换问题,这需要对原有的方案进行大幅度修改,风险太大; // 2.用 cookie 记录主题并再现,很容易出现 Html 预览选项卡上选定的主题与预览的效果相反的问题!! // ——这势必让用户认为这是个Bug。 // 所以综合考虑后,决定放弃这个功能的实现——这年头硬盘空间不值钱,编译两个主题的CHM文件也占不了多大空间。 #region 链接主题CSS文件 // 主题 CSS 文件由两个主题(共六个文件)构成。 // 其中,基于演示模式的需要,主CSS文件分两个版本(演示版本、教程版本); // 左边栏 CSS 文件则不分演示模式与普通教程模式。 // 也就是说: // presentation_light.css和lesson_light.css是不会同时使用的; // 并且 presentation_light.css不会用于编译工作区,更不会用在CHM文件中。 // 此外,每套主题的三个CSS文件都支持用户自定义——只需要将自定义CSS文件命名为: // “custom_{原文件名}”的形式即可。 // 由于后添加的链接引用的CSS中定义的样式会覆盖先链接的CSS文件中定义的样式, // 所以这样不但可以实现用户自定义CSS样式,而且还并不需要用户实现每一个样式——便于用户微调页面效果。 // 至于到底选择 presentation_light.css(lesson_light.css)还是presentation_dark.css(lesson_dark.css), // 则是由“Globals.MainWindow.ThemeText”决定的, // 而“Globals.MainWindow.ThemeText”这个属性值由右工具栏“Html 预览”选项卡上的主题切换框的当前值决定。 // 链接 主CSS文件 string themeCssPrefix = (withoutAdonors ? "presentation_" : "lesson_"); String themeCssLinkText = "<link id='themeLink' rel=\"stylesheet\" href=\"" + directoryLevelMark + themeCssPrefix + Globals.MainWindow.ThemeText + ".css\" type=\"text/css\">\n"; // 链接用户自定义的 主CSS文件 String customThemeCssLinkText = ""; string customThemeCssFileName = "custom_" + themeCssPrefix + Globals.MainWindow.ThemeText + ".css"; if (File.Exists(Globals.PathOfWorkspace + customThemeCssFileName)) { customThemeCssLinkText = "<link id='themeLink' rel=\"stylesheet\" href=\"" + directoryLevelMark + customThemeCssFileName + "\" type=\"text/css\">\n"; } // 链接 左边栏菜单的CSS文件 String menuThemeCssLinkText = "<link rel=\"stylesheet\" href=\"" + directoryLevelMark + "menu_" + Globals.MainWindow.ThemeText + ".css\" type=\"text/css\" />\n"; // 链接用户自定义的 左边栏菜单的CSS文件 String customMenuThemeCssLinkText = ""; string customMenuThemeCssFileName = "custom_" + "menu_" + Globals.MainWindow.ThemeText + ".css"; if (File.Exists(Globals.PathOfWorkspace + customMenuThemeCssFileName)) { customMenuThemeCssLinkText = "<link rel=\"stylesheet\" href=\"" + directoryLevelMark + customMenuThemeCssFileName + "\" type=\"text/css\" />\n"; } #endregion string script = "<script>\n" + "function ShowPage()\n" + "{\n" + "document.getElementById('content').style.visibility = \"visible\";\n" //+ $"document.body.style.zoom = {webBrowserZoom};\n" + $"document.body.style.fontSize = {16 * webBrowserZoom}+'px';\n" //只有在这里想办法才可能解决IE 11下body元素向右偏移面能自动居中对齐的问题。 //网上找到的通过style等途径设置居中都无效。 //+ "var pageWidth = window.screen.availWidth ;" //+ "var bodyWidth = document.body.scrollWidth;" //+ "var leftMargin = (pageWidth - bodyWidth)/2;" //+ "document.getElementsByTagName('body')(0).style.marginLeft = leftMargin;" //+ "document.getElementsByTagName('body')(0).style.marginRight = leftMargin;" //+ "alert(\"ClientWidth\"+document.body.clientWidth+\"|OffsetWidth\"+document.body.offsetWidth+\"|scrollWidth\"+document.body.scrollWidth+\"|screenwidth\"+window.screen.width+\"|availWidth\"+window.screen.availWidth );" + "}\n" + "function HidePage()\n" + "{\n" + "document.getElementById('content').style.visibility = \"hidden\";\n" + "}\n" + "</script>\n"; string timeText = ""; if (Globals.MainWindow.AppendTimeOfCompiling) { timeText = "<p id =\"compile_time\">" + DateTime.Now.ToString() + "</p>"; } string pagefooterString = "<div class=\"foot\"><p id=\"author\">" + footerText //这是文档的脚注文本,一般可以填组织、作者名称什么的。 + $"</p>{timeText}</div>"; var charset = Globals.MainWindow.DefaultEncoding;//"gb2312";//"utf-8"; //解决总是浏览网页时总是显示“为保护……你的 Web 浏览器已经限制此文件……”警示信息条。 //<!-- saved from url=(0014)about:internet -->必须在Html之后Head之前,否则Microsoft Edge中无法正确显示。 //<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"> string htmlDocumentText = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">" + "\n\n<html xmlns=\"http://www.w3.org/1999/xhtml\" >\n" //测试用:style=\"background:red;\" + "<!-- saved from url=(0016)http://localhost -->\r\n" //旧版:+ "<!-- saved from url=(0014)about:internet -->\n" + "<head>" + "<title>" + htmlDocumentTitle + "</title>\n" + "<meta name=\"info\" content=\"" + shortFileName + "\" >" + "<meta name=\"viewport\" content=\"width=device-width, user-scalable=yes\" />" + "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=" + charset + "\">" + themeCssLinkText + customThemeCssLinkText //之所以在用户自定义CSS文件存在时仍然引用LME提供的CSS是防止用户编写的CSS有问题。保留LME的CSS可以减少错误的概率。 + (compilePageMenu ? (menuThemeCssLinkText + customMenuThemeCssLinkText) : "") //注意linkCss2c和linkCss3c的顺序不能在前。 + script + headStyleTexts //旧版(测试):+ "</head><body onLoad=\"HidePage()\" style=\"background:green;\">"//overflow-y:hidden; scroll=\"no\"//隐藏垂直滚动条不好。 + "</head><body " + (withoutAdonors ? " style=\"overflow-x:auto;\" " : "") + " onLoad=\"ShowPage()\">" + "<p style=\"visibility: collapse;\"><span id=\"__TOP_4E4ABC53-B143-46FF-93CF-F9381EAD8E14__\" class=\"anchor\"></span></p>" //这个锚总是存在的。 + (withoutAdonors ? "" : "<div style=\"text-align:right\"><span class=\"pageheader\">") + (withoutAdonors ? "" : htmlDocumentPageHeader) + (withoutAdonors ? "" : "</span><a onclick=\"print()\" style=\"visibility: hidden\">"/*打印〔此功能会丢失内容〕...*/+ "</a></div>") + "<div id=\"printArea\">" + (withoutAdonors ? "" : "<p class='fileheader' id='file_header'>") + (withoutAdonors ? "" : htmlDocumentTitle) //这是整个文档的标题,以%开头。 + (withoutAdonors ? "" : "</p><hr />") + "<div id=\"content\">" + htmlBody + "<script></script>" + (compilePageMenu ? "<p class=\"back_to_top_link\"><a href=\"#__TOP_4E4ABC53-B143-46FF-93CF-F9381EAD8E14__\">回到顶部</a></p>" : "") + "</div>" + (withoutAdonors ? "" : "<br /><br /><br /><br /><br /><hr />") + (withoutAdonors ? "" : pagefooterString) + (addBackToIndexLink ? ("<p style=\"margin-left:10px;margin-right:10px;text-indent:0px;\">" + previewPageLink + " " + BuildIndexLink(srcFilePath) + " " + nextPageLink + "</p>") : "") + "</div>" + $"<br/><br/><br/><br/><br/>\n{bodyEndScripts}\n</body></html>";//加五个空行是为了让让用户更明白地意识到页面结束了。 htmlTitle = htmlDocumentTitle; //对编译后的html格式化。 if (Globals.MainWindow.FormatAfterCompile) { NSoup.Nodes.Document doc = NSoup.NSoupClient.Parse(htmlDocumentText); htmlDocumentText = doc.OuterHtml(); } var newFileName = srcFilePath.Substring(0, srcFilePath.Length - 3) + ".html"; if (charset == "utf-8") { using (StreamWriter stream = new StreamWriter(newFileName, false, Encoding.UTF8)) { stream.Write(htmlDocumentText); } } else if (charset == "gb2312") { using (StreamWriter stream = new StreamWriter(newFileName, false, System.Text.Encoding.GetEncoding("gb2312"))) { stream.Write(htmlDocumentText); } } return newFileName; } return null; } /// <summary> /// 取出用户自己在文档内部写的 <Script>...</Script> 块。 /// </summary> private static string GetPageScripts(string contentText, out string ScriptTexts) { if (string.IsNullOrWhiteSpace(contentText)) { ScriptTexts = ""; return contentText; } //string startRegionScript = @"^[<][Ss][Cc][Rr][Ii][Pp][Tt]([ ]{1,}.*)?[>]"; //string endRegionScript = @"^[<][/][Ss][Cc][Rr][Ii][Pp][Tt][>]"; var reg = new Regex(@"(^[<][Ss][Cc][Rr][Ii][Pp][Tt]([ ]{1,}.*)?[>])[\s\S]*?([<][/][Ss][Cc][Rr][Ii][Pp][Tt][>])", RegexOptions.Multiline); var matches = reg.Matches(contentText); if (matches.Count <= 0) { ScriptTexts = ""; return contentText; } StringBuilder sb = new StringBuilder(); var splitter = new char[] { '\r', '\n', }; foreach (Match match in matches) { if (match.Length <= 0) continue; var lines = match.Value.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); if (lines.Length <= 0) continue; StringBuilder sbIn = new StringBuilder(); foreach (var line in lines) { if (IsCommentLine(line)) continue; //{ // sbIn.Append($"<!--{line}-->\n"); //} //为什么不用这个办法? //这是因为低版本的浏览器不支持内嵌代码,所以会将内部的 Script 标签及其内容显示在页面上。 //为避免此现象出现,常常会写成下面这样: //<Script> //<!-- // ... //--> //</Script> //这样做的目的是:如果浏览器不支持这样的内部样式表,就将其中的内容视为Html注释。 //而如果将冒号开头的注释行也改写成 Html 样式的注释,就会产生冲突。 sbIn.Append($"{line}\n"); } sb.Append(sbIn.ToString() + "\n"); } ScriptTexts = sb.ToString(); StringBuilder result = new StringBuilder(); result.Append(contentText.Substring(0, matches[0].Index)); result.Append("\r\n"); for (int i = 0; i < matches.Count - 1; i++) { var thisMatch = matches[i]; var nextMatch = matches[i + 1]; var thisEnd = thisMatch.Index + thisMatch.Length; result.Append(contentText.Substring(thisEnd, nextMatch.Index - thisEnd)); result.Append("\r\n"); } var lastMatch = matches[matches.Count - 1]; result.Append(contentText.Substring(lastMatch.Index + lastMatch.Length)); result.Append("\r\n"); return result.ToString(); } /// <summary> /// 取出用户自己在文档内部写的 <Style>...</Style> 块。 /// </summary> private static string GetPageStyles(string contentText, out string styleTexts) { if (string.IsNullOrWhiteSpace(contentText)) { styleTexts = ""; return contentText; } //string startRegionStyle = @"^[<][Ss][Tt][Yy][Ll][Ee]([ ]{1,}.*)?[>]"; //string endRegionStyle = @"^[<][/][Ss][Tt][Yy][Ll][Ee][>]"; var reg = new Regex(@"(^[<][Ss][Tt][Yy][Ll][Ee]([ ]{1,}.*)?[>])[\s\S]*?([<][/][Ss][Tt][Yy][Ll][Ee][>])", RegexOptions.Multiline); var matches = reg.Matches(contentText); if (matches.Count <= 0) { styleTexts = ""; return contentText; } StringBuilder sb = new StringBuilder(); var splitter = new char[] { '\r', '\n', }; foreach (Match match in matches) { if (match.Length <= 0) continue; var lines = match.Value.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); if (lines.Length <= 0) continue; StringBuilder sbIn = new StringBuilder(); foreach (var line in lines) { if (IsCommentLine(line)) continue; //{ // sbIn.Append($"<!--{line}-->\n"); //} //为什么不用这个办法? //这是因为低版本的浏览器不支持这样的内部样式表,所以会将内部的 Style 标签及其内容显示在页面上。 //为避免此现象出现,常常会写成下面这样: //<Style> //<!-- // body { background: red;} //--> //</Style> //这样做的目的是:如果浏览器不支持这样的内部样式表,就将其中的内容视为Html注释。 //而如果将冒号开头的注释行也改写成 Html 样式的注释,就会产生冲突。 sbIn.Append($"{line}\n"); } sb.Append(sbIn.ToString() + "\n"); } styleTexts = sb.ToString(); StringBuilder result = new StringBuilder(); result.Append(contentText.Substring(0, matches[0].Index)); result.Append("\r\n"); for (int i = 0; i < matches.Count - 1; i++) { var thisMatch = matches[i]; var nextMatch = matches[i + 1]; var thisEnd = thisMatch.Index + thisMatch.Length; result.Append(contentText.Substring(thisEnd, nextMatch.Index - thisEnd)); result.Append("\r\n"); } var lastMatch = matches[matches.Count - 1]; result.Append(contentText.Substring(lastMatch.Index + lastMatch.Length)); result.Append("\r\n"); return result.ToString(); } /// <summary> /// 如果段落中只有一个 Img 标签,则将此 Imag 标签从段落中独立出来。这样可以实现图像本身的对齐效果。 /// </summary> /// <param name="match"></param> /// <returns></returns> public static string FormatParagraphsTag(Match match) { if (match.Length < 7) return match.Value; return match.Value.Substring(3, match.Length - 7); } private static object BuildIndexLink(string srcFilePath) { if (srcFilePath.ToLower().StartsWith(Globals.PathOfWorkspace.ToLower())) { var tail = srcFilePath.Substring(Globals.PathOfWorkspace.Length); if (tail.StartsWith("\\") || tail.StartsWith("/")) { tail = tail.Substring(1); } var spans = tail.Split(new char[] { '\\', '/' }, StringSplitOptions.None); StringBuilder sb = new StringBuilder(); for (int i = 1; i < spans.Length; i++)//注意i从1开始,不然目录会多一层。 { sb.Append("../"); } return $"<a href=\"{sb.ToString()}_index.html\">回目录</a>"; } return ""; } /// <summary> /// 判断指定文件是否被加密。 /// </summary> /// <param name="srcFilePath">Markdown 文件的路径。</param> /// <param name="password">用以传出加密文件的密码。</param> /// <param name="passwordTip">用以传出加密文件的密码提示信息。</param> /// <returns></returns> public static bool IsFileEncrypted(string srcFilePath, out string password, out string passwordTip) { if (File.Exists(srcFilePath) == false) { password = null; passwordTip = null; return false; } using (var sr = new StreamReader(srcFilePath)) { var fstLine = sr.ReadLine(); if (string.IsNullOrWhiteSpace(fstLine)) { passwordTip = password = ""; return false; } var decryptedFstLine = SetPasswordPanel.TextDecrypt( fstLine.Replace("[[<r>]]", "\r").Replace("[[<n>]]", "\n").ToCharArray(), "DyBj#PpBb"); var indexOfPassword = decryptedFstLine.IndexOf("Password:"); var indexOfQuestion = decryptedFstLine.IndexOf("Question:"); password = ""; passwordTip = ""; if (indexOfQuestion >= 0 && indexOfPassword >= 10) { password = decryptedFstLine.Substring(indexOfPassword + 9); passwordTip = decryptedFstLine.Substring(9, indexOfPassword - 10); return true; } } return false; } /// <summary> /// 判断此磁盘文件第一行是否带“废弃”标记文本。 /// </summary> /// <param name="srcFilePath"></param> /// <returns></returns> public static bool IsFileAborted(string srcFilePath) { bool result = false; if (File.Exists(srcFilePath) == false) return false; using (var sr = new StreamReader(srcFilePath)) { var fstLine = sr.ReadLine(); if (string.IsNullOrWhiteSpace(fstLine)) return false; result = IsAbortedTaskLine(fstLine); } return result; } /// <summary> /// 取当前路径相对于当前工作区路径的层级差。 /// </summary> /// <param name="fullFilePath">文件路径。</param> public static int GetSubLayerOfDirectory(string fullFilePath) { if (string.IsNullOrEmpty(fullFilePath)) return -1; if (fullFilePath.ToLower().StartsWith(Globals.PathOfWorkspace.ToLower()) == false) return -1; var tailTextOfPath = fullFilePath.Substring(Globals.PathOfWorkspace.Length); int i = 0; foreach (char c in tailTextOfPath) { if (c == '\\') { i++; } } return i; } /// <summary> /// 根据文件路径与当前工作区路径之间的层级差来生成相对引用字符串前缀。 /// </summary> /// <param name="fullFilePath">文件路径。</param> public static string GetDirectoryLevelMark(string fullFilePath) { StringBuilder sb = new StringBuilder(); for (int i = 0; i <= GetSubLayerOfDirectory(fullFilePath) - 1; i++) { sb.Append("../"); } return sb.ToString(); } } /// <summary> /// 此类用于在编译处理 <h1></h1>...<h6></h6>,以便使其这些标题支持折叠、自动编号等功能。 /// </summary> public class HtmlHeaderInfo { /// <summary> /// 此标题所需要的 JavaScript。 /// </summary> public string JavaScriptText { get; set; } = ""; /// <summary> /// 此标题之前所有标题对应 Div 的结尾字符串。因为前面可能有几层,所以可能像这样:“</div></div></div>”。 /// </summary> public string PreviewHeaderPanelCloseDivsMark { get; set; } = ""; /// <summary> /// 转换后的文本。 /// </summary> public string NewText { get; set; } = ""; /// <summary> /// 此标题是否已关闭。 /// </summary> public bool IsClosed { get; set; } = false; /// <summary> /// 标题文本。 /// </summary> public string Header { get; set; } = ""; /// <summary> /// Html文本。 /// </summary> public string Html { get; set; } = ""; /// <summary> /// 标题层级。 /// </summary> public int Level { get; set; } = 0; /// <summary> /// 从 Html 在提取出来的正则匹配信息。 /// </summary> public Match Match { get; set; } = null; } }