using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace LunarSF.SHomeWorkshop.LunarMarkdownEditor { /// 由BaseQuestion拆分而成,如果一个Base中只有一个填充项,则会被拆分成一个Question。 /// BaseQuestion的Body(题干)中有几个填空项,就应该有几个答案列表。 public class Question { private List<ChoiceItem> choiceItems = new List<ChoiceItem>(); /// <summary> /// 选择支列表。 /// </summary> public List<ChoiceItem> ChoiceItems { get { return this.choiceItems; } } private string answer = string.Empty; /// <summary> /// [只读]仅适用于选择题(包括判断题)、填空题。不适用于主观题。 /// </summary> public string Answer { get { if (choiceItems.Count <= 0) { if (string.IsNullOrEmpty(this.titleString)) return string.Empty; //填空题 int startBracket = this.titleString.IndexOf("【"); int closeBracket = this.titleString.IndexOf("】"); if (startBracket < 0 || closeBracket < 0 || closeBracket < startBracket) return string.Empty; return this.titleString.Substring(startBracket, closeBracket - startBracket); } return this.answer; } } /// <summary> /// [构造方法]用于生成一道只有题干的试题。 /// </summary> /// <param name="titleString">试题的题干文本。</param> public Question(string titleString) { this.TitleString = titleString; } /// <summary> /// [构造方法]生成一道试题。适用于填空题(无答案)、选择题(有答案项、错项,)、判断题(特殊的选择题)。 /// </summary> /// <param name="titleString">应传入由BaseQuestion.BuildQuestionTitles()方法生成的String[]的某个成员。 /// 它的形式是一行带一个填空的文本。</param> /// <param name="bodyString">应传入每个试题的原文本的“答案”部分, /// 每个答案文本片段中应包含一个“答案”及一个或多个“错项”。</param> public Question(string titleString, string bodyString) { this.TitleString = titleString; if (string.IsNullOrEmpty(bodyString)) return;//填空题,有填充项而无答案项。 // 根据bodyStrings生成BaseQuestionItems String[] itemStrings = bodyString.Split(new string[1] { "错项>>" }, StringSplitOptions.None); // 第一个片段是答案,其余每个片段都是“错项”。 if (itemStrings.Length > 1) { ChoiceItem answerChoiceItem = new ChoiceItem(); answerChoiceItem.IsAnswer = true; int indexOfAnalysis = itemStrings[0].IndexOf("解析>>"); if (indexOfAnalysis > -1) { answerChoiceItem.Text = itemStrings[0].Substring(0, indexOfAnalysis); answerChoiceItem.AnalysisText = L.FormatChoiceQuestionTextString(itemStrings[0].Substring(indexOfAnalysis + 4)); } else { answerChoiceItem.Text = itemStrings[0]; } this.choiceItems.Add(answerChoiceItem); this.answer = answerChoiceItem.Text; for (int ii = 1; ii < itemStrings.Length; ii++) { ChoiceItem otherChoiceItem = new ChoiceItem(); indexOfAnalysis = -1; // 索引从1开始。 indexOfAnalysis = itemStrings[ii].IndexOf("解析>>"); if (indexOfAnalysis > -1) { otherChoiceItem.Text = L.FormatChoiceQuestionTextString(itemStrings[ii].Substring(0, indexOfAnalysis)); otherChoiceItem.AnalysisText = L.FormatChoiceQuestionTextString(itemStrings[ii].Substring(indexOfAnalysis + 4)); } else { otherChoiceItem.Text = L.FormatChoiceQuestionTextString(itemStrings[ii]); } this.choiceItems.Add(otherChoiceItem); } } // 使用随机数混淆顺序。 int[] random = L.GetRandomIntArray(choiceItems.Count); for (int i = 0; i < choiceItems.Count; i++) { choiceItems[i].OrderNumber = random[i]; } choiceItems.Sort(new CmparsionQuestionItem()); } /// <summary> /// [构造方法]适用于 /// </summary> /// <param name="titleString"></param> /// <param name="titleAndAnswers"></param> public Question(string titleString, string[] titleAndAnswers) { this.titleString = titleString; this.answers.Clear(); for (int i = 1; i < titleAndAnswers.Length; i++) { this.answers.Add(titleAndAnswers[i]); } } /// <summary> /// 试题类型:判断、选择、主观。 /// </summary> public QuestionType Type { get { if (this.choiceItems.Count <= 0) { if (this.answers.Count > 0) return QuestionType.Subjective; if (this.titleString.Contains("【") == false || this.titleString.Contains("】") == false) return QuestionType.Text;//没有任何填空项,只呈现纯文本。 return QuestionType.FillBlank; } else { //判断题是只有两个选项的选择题,且两个选项必须是:一个“正确”、一个“错误”。 if (this.choiceItems.Count == 2) { if ((this.choiceItems[0].Text == "正确" && this.choiceItems[1].Text == "错误") || (this.choiceItems[0].Text == "错误" && this.choiceItems[1].Text == "正确")) return QuestionType.Judge; } return QuestionType.Choice; } } } /// <summary> /// 判断文本行是否属于与试题相关的行。 /// </summary> /// <param name="lineText">源文本。</param> /// <returns></returns> public static bool IsExamTextLine(string lineText) { return (lineText.StartsWith(" 试题>>") || lineText.StartsWith("<<材料>>") || lineText.StartsWith("<<出处>>") || lineText.StartsWith("<<问题>>") || lineText.StartsWith(" 答案>>") || lineText.StartsWith(" 解析>>") || lineText.StartsWith(" 错项>>")); } /// <summary> /// 将试题文本编译为Html标签文本。(编译试题) /// </summary> /// <param name="questionText">试题内容文本。</param> public static string ConvertQuestionsToHtml(string questionText) { if (string.IsNullOrEmpty(questionText)) return ""; //考虑到Markdown与TestPaper的混杂难以处理,应先将试题处理为html,期间还需要考虑图像引用问题。 //注意:所有在试题影响范围内的文本均算是试题的一部分,这样试题就能支持多行文本了。 //填空题比较复杂: // ⑴如果填空题后面找不到“〓〓〓〓〓〓”结尾,则填空题只有一行文本。 // ⑵如果填空题后能找到试题结束标记“〓〓〓〓〓〓”,则到这个标记的所有文本均算是这个试题的内容。 if (questionText.StartsWith(" 试题>>")) { questionText = "\n" + questionText; } var startIndex = questionText.IndexOf("\n 试题>>");//必须是在文本行开头才能算! if (startIndex < 0) return questionText;//没有试题,原样输出。 StringBuilder resultBuilder = new StringBuilder(); List<Question> questions = new List<Question>(); string temp = questionText.Replace("\n 试题>>", "\n <<&LINE&>>试题>>"); var sections = temp.Split(new string[] { "<<&LINE&>>" }, StringSplitOptions.None); int startNumber = 1; foreach (var s in sections) { if (s.StartsWith("试题>>")) { //这个字段中包含试题文本,需要特殊处理 //截取出试题结束符后的文本,这部分不是试题的组成部分,直接附加即可。 string header = s; string tail = ""; var tailIndex = s.IndexOf("〓〓〓〓〓〓"); if (tailIndex >= 0) { tail = s.Substring(tailIndex + 6); header = s.Substring(0, tailIndex); } header = ConvertQuestionsToHtml(header, ref startNumber); resultBuilder.Append("\n\n"); resultBuilder.Append(header); resultBuilder.Append("\n\n"); resultBuilder.Append(tail); } else { //不需要特殊处理。 //Markdown中,空行、空格都是有用处的,不能随意去除。 //resultBuilder.Append(FormatText(s)); resultBuilder.Append(s); } } //Markdown中,空行、空格都是有用处的,不能随意去除。 //return FormatText(resultBuilder.ToString()); return resultBuilder.ToString(); } /// <summary> /// 文本格式化,去除每行首尾的空白字符,统一换行符为“\n”。 /// </summary> /// <param name="sourceText">源文本。</param> private static string FormatText(string sourceText) { if (string.IsNullOrEmpty(sourceText)) return ""; StringBuilder sb = new StringBuilder(); var lines = sourceText.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { string s = line.Trim(new char[] { '\t', ' ', ' ' }); sb.Append(s + (s.Length <= 0 ? "" : "\n")); } var result = sb.ToString(); if (result.EndsWith("\n")) { return result.Substring(0, result.Length - 1); } else return result; } /// <summary> /// 将试题文本转换为Html。(试题编译) /// </summary> /// <param name="questionText">试题文本。</param> /// <param name="startNumber">试题序号。</param> private static string ConvertQuestionsToHtml(string questionText, ref int startNumber) { if (string.IsNullOrEmpty(questionText)) return ""; List<Question> newQuestions = BaseQuestion.BuildQuestionList(questionText); if (newQuestions == null || newQuestions.Count <= 0) return ""; StringBuilder sb = new StringBuilder(); StringBuilder endScript = new StringBuilder(); var hideText = ""; if (Globals.MainWindow.HideExamAnswer) { hideText = " style = 'display: none;'"; } for (int i = 0; i < newQuestions.Count; i++) { var q = newQuestions[i]; switch (q.Type) { case QuestionType.Judge://判断题是特殊形式的选择题。 case QuestionType.Choice: { var indexOfStart = q.TitleString.IndexOf("【"); var indexOfEnd = q.TitleString.IndexOf("】"); if (indexOfStart < 0 || indexOfEnd < 0 || indexOfEnd <= indexOfStart) continue;//选择题无填充项,则无法确定答案。 //var fillblank = ""; //fillblank = q.TitleString.Substring(indexOfStart + 1, indexOfEnd - indexOfStart - 2); //var displayTitle = q.TitleString.Replace("【", "<code class='exam_fb'>").Replace("】", "</code>"); var displayTitle = q.TitleString.Substring(0, indexOfStart + 1) + " " + q.TitleString.Substring(indexOfEnd); sb.Append("<div class='ex_c'>"); sb.Append("<p class='ex_c_t'>" + startNumber.ToString() + "." + FormatText(displayTitle) + "</p>"); //嵌入标题中的图像文件链接 if (string.IsNullOrWhiteSpace(q.MarkdownImagePath) == false) { sb.Append($"<p><img src=\"{q.MarkdownImagePath}\" alt=\"{q.MarkdownImageTooltip}\"/></p>"); } startNumber++; sb.Append("<ol class='ex_c_xx' type='A'>"); //TODO:还缺混淆:q.ChoiceItems.Sort(); endScript.Append("<script>"); for (int m = 0; m < q.ChoiceItems.Count; m++) { var item = q.ChoiceItems[m]; sb.Append($"<li id='li_{startNumber}_{m}'>" + FormatText(item.Text) + "</li>"); sb.Append($"<div class='ex_c_jx' id='jx_{startNumber}_{m}'{hideText}><p>" + (item.IsAnswer ? "<span class='ex_c_right'>答 案</span>" : "<span class='ex_c_err'>非答案</span>") + FormatText(item.AnalysisText) + "</p></div>"); endScript.Append( "$(document).ready(function() {" + $@"$('#li\_{startNumber}\_{m}').click(function() {{" + $@"$('#jx\_{startNumber}\_{m}').toggle();" + "});" + "});" ); } sb.Append("</ol>"); sb.Append("</div>"); endScript.Append("</script>"); break; } case QuestionType.FillBlank: { var titleSpans = q.TitleString.Replace("【", "[[^]]【").Split(new string[] { "[[^]]", "】" }, StringSplitOptions.None); var title = new StringBuilder(); var fillblanks = new StringBuilder(); int num = 0; foreach (var s in titleSpans) { if (s.StartsWith("【")) { num++; title.Append("【<u> [" + num + "] </u>】"); fillblanks.Append("[" + num + "]" + s.Substring(1)); fillblanks.Append(" "); } else { title.Append(s); } } var fillBlanksText = fillblanks.ToString(); if (fillBlanksText.EndsWith(" ")) { fillBlanksText = fillBlanksText.Substring(0, fillBlanksText.Length - 1); } sb.Append("<div class='ex_f'>"); sb.Append("<p class='ex_f_t'>" + startNumber.ToString() + "." + FormatText(title.ToString()) + "</p>"); //嵌入标题中的图像文件链接 if (string.IsNullOrWhiteSpace(q.MarkdownImagePath) == false) { sb.Append($"<p><img src=\"{q.MarkdownImagePath}\" alt=\"{q.MarkdownImageTooltip}\"/></p>"); } sb.Append($"<p class='ex_f_da'><span class='ex_f_da' id='span_ah_{startNumber}'>答案</span><span id='span_at_{startNumber}'{hideText}>" + fillBlanksText + "</span></p>"); endScript.Append("<script>"); endScript.Append( "$(document).ready(function() {" + $@"$('#span\_ah\_{startNumber}').click(function() {{" + $@"$('#span\_at\_{startNumber}').toggle();" + "});" + "});" ); endScript.Append("</script>"); startNumber++; sb.Append("</div>"); break; } case QuestionType.Subjective: { sb.Append("<div class='ex_m'>"); if (q.Materials != null && q.Materials.Count > 0) { sb.Append("<p class='ex_m_dy'>" + startNumber.ToString() + ".阅读下列材料:</p>"); } else { sb.Append("<p class='ex_m_dy'>" + startNumber.ToString() + ".</p>"); } startNumber++; sb.Append("<div class='ex_m_cl'>\n"); foreach (var s in q.Materials) { int indexOfcc = s.IndexOf("<<出处>>"); if (indexOfcc >= 0) { sb.Append("<p class='ex_m_cl'>" + FormatText(s.Substring(0, indexOfcc)) + "</p>"); string clcc = s.Substring(indexOfcc + 6); if (clcc.StartsWith("——") == false) { clcc = "——" + clcc; } sb.Append("<p class='ex_m_cc'>" + FormatText(clcc) + "</p>"); } else { sb.Append("<p class='ex_m_cl'>" + FormatText(s) + "</p>"); } } sb.Append("</div>");//<div class='ex_m_cl'>,材料块。 sb.Append("<p>请回答:</p>"); //问题部分 for (int j = 0; j < q.AsksList.Count; j++) { var s = q.AsksList[j]; sb.Append("<p class='ex_m_q'>(" + (j + 1).ToString() + ")" + FormatText(s) + "<p>"); } sb.Append($"<p class='ex_m_dy'><span class='ex_m_answer' id='ex_m_answer_{startNumber}'>参考答案</span></p>"); //下面是答案部分。 for (int j = 0; j < q.Answers.Count; j++) { var s = q.Answers[j]; int indexOfjx = s.IndexOf("解析>>"); endScript.Append("<script>"); if (indexOfjx >= 0) { //注意:Markdown编译器在处理带下划线的字符串时遵循如下规则: // 如果该下划线是在某个Html Tag的内部,则原样输出; // 如果该下划线不是某个Html Tag的内部(例如在Js代码中),要转义才能输出,不转义会被解释为倾斜效果。 sb.Append($"<p class='ex_m_da'{hideText} id='ex_m_da_{startNumber}_{j}'>(" + (j + 1).ToString() + ")" + FormatText(s.Substring(0, indexOfjx)) + "</p>"); sb.Append($"<p class='ex_m_jx'{hideText} id='ex_m_jx_{startNumber}_{j}'><span class='ex_m_analysis'> 解析</span>" + FormatText(s.Substring(indexOfjx + 6)) + "</p>"); endScript.Append( "$(document).ready(function() {" + $@"$('#ex\_m\_da\_{startNumber}\_{j}').click(function() {{" + $@"$('#ex\_m\_da\_{startNumber}\_{j}').toggle();" + "});" + "});" );//点击隐藏自身 endScript.Append( "$(document).ready(function() {" + $@"$('#ex\_m\_jx\_{startNumber}\_{j}').click(function() {{" + $@"$('#ex\_m\_jx\_{startNumber}\_{j}').toggle();" + "});" + "});" );//点击隐藏自身 endScript.Append( "$(document).ready(function() {" + $@"$('#ex\_m\_answer\_{startNumber}').click(function() {{" + $@"$('#ex\_m\_da\_{startNumber}\_{j}').toggle();" + $@"$('#ex\_m\_jx\_{startNumber}\_{j}').toggle();" + "});" + "});" );//点击显示或隐藏下属的答案、解析 } else { sb.Append($@"<p class='ex_m_da'{hideText} id='ex_m_da_{startNumber}_{j}'>(" + (j + 1).ToString() + ")" + FormatText(s) + "</p>"); endScript.Append( "$(document).ready(function() {" + $@"$('#ex\_m\_da\_{startNumber}\_{j}').click(function() {{" + $@"$('#ex\_m\_da\_{startNumber}\_{j}').toggle();" + "});" + "});" );//点击隐藏自身 endScript.Append( "$(document).ready(function() {" + $@"$('#ex\_m\_answer\_{startNumber}').click(function() {{" + $@"$('#ex\_m\_da\_{startNumber}\_{j}').toggle();" + "});" + "});" );//点击显示或隐藏下属的答案、解析 } endScript.Append("</script>"); } sb.Append("</div>"); break; } case QuestionType.Text: default: { return questionText; } } } var end = endScript.ToString(); sb.Append(endScript); return sb.ToString(); } /// <summary> /// 此方法用于从 Markdown 格式的图像链接文本中取出链接到的图像的路径。 /// 试题题干中可以嵌入 Markdown 格式的图像链接。这样试题就可以支持图像了。 /// </summary> /// <param name="imageLinkText">Markdown 格式的图像链接文本。</param> /// <returns>去除其它信息,只保留图像地址。</returns> public static string GetMarkdownIamgePath(string imageLinkText) { if (string.IsNullOrWhiteSpace(imageLinkText)) return null; //Regex regex = new Regex("(?<=\\!\\[.*\\]\\().*(?=[ \"\\)])"); int leftBracketIndex; int rightBracketIndex; leftBracketIndex = imageLinkText.IndexOf("]("); if (leftBracketIndex < 0) return null; rightBracketIndex = imageLinkText.IndexOf(" \"", leftBracketIndex); if (rightBracketIndex < 0) rightBracketIndex = imageLinkText.IndexOf(")", leftBracketIndex); if (rightBracketIndex < 0) return null; if (rightBracketIndex <= leftBracketIndex) return null; return imageLinkText.Substring(leftBracketIndex, rightBracketIndex - leftBracketIndex).Trim(new char[] { ' ', ' ' }); } /// <summary> /// 取 Markdown 图像链接文本。用在题干文本中提取图像链接。 /// 格式如下: ///  /// </summary> /// <param name="text">题干文本。</param> /// <param name="header">传出 Markdown 图像链接中的图像标题。</param> /// <param name="path">传出 Markdown 图像链接中的链接。</param> /// <param name="tooltip">传出 Markdown 图像链接中的提示部分。</param> /// <returns>顺利完成时返回 string.Empty。</returns> public static string GetMarkdownImageLinkText(string text, out string header, out string path, out string tooltip) { if (string.IsNullOrWhiteSpace(text)) { header = string.Empty; path = string.Empty; tooltip = string.Empty; return string.Empty; } var start = text.IndexOf("; if (index < 0) { header = string.Empty; tooltip = string.Empty; path = string.Empty; return string.Empty; } header = result.Substring(2, index - 2); var newEnd = result.IndexOf(" \"", index); if (newEnd < 0) { newEnd = result.Length; path = result.Substring(index + 2, newEnd - index - 3).Trim(new char[] { ' ', ' ', '\t' });//最后是个")"。 tooltip = string.Empty; } else { path = result.Substring(index + 2, newEnd - index - 2).Trim(new char[] { ' ', ' ', '\t' });//最后是个")"。 var newEnd2 = result.IndexOf("\")", newEnd); if (newEnd2 < 0 || newEnd2 < newEnd + 2) { tooltip = string.Empty; } else { tooltip = result.Substring(newEnd + 2, newEnd2 - newEnd - 2); } } return result; } #region 主观题使用的几个列表。 private List<string> materials = new List<string>(); /// <summary> /// 材料列表。 /// </summary> public List<string> Materials { get { return this.materials; } } private List<string> asksList = new List<string>(); /// <summary> /// 问题列表。 /// </summary> public List<string> AsksList { get { return this.asksList; } } private List<string> answers = new List<string>(); /// <summary> /// [只读]此属性仅适用于主观题。 /// </summary> public List<string> Answers { get { return this.answers; } } #endregion private string titleString; /// <summary> /// 题干文本。 /// </summary> public string TitleString { get { string result = this.titleString; ; Regex regex = new Regex(@"!\[.*\]\(.*\)"); var martch = regex.Match(this.titleString); if (martch != null && martch.Success) { string shortName = "右图"; var link = martch.Value; Regex regex2 = new Regex(@"\[.*\]"); var martch2 = regex2.Match(link); if (martch2 != null && martch2.Success && martch2.Length > 2) { shortName = martch2.Value.Substring(1, martch2.Length - 2); } result = this.titleString.Substring(0, martch.Index) + "《" + shortName + "》" + this.titleString.Substring(martch.Index + martch.Length); return result; } return result; } set { this.titleString = value; string header; string path; string tooltip; this.markdownLinkText = GetMarkdownImageLinkText(value, out header, out path, out tooltip); this.markdownImageHeader = header; this.markdownImagePath = path; this.markdownIamgeTooltip = tooltip; } } private string markdownImagePath = ""; /// <summary> /// 通过 Markdown 格式引用的图像链接的路径。 /// </summary> public string MarkdownImagePath { get { return this.markdownImagePath; } } private string markdownImageHeader = ""; /// <summary> /// 通过 Markdown 格式引用的图像链接的标题文本。 /// </summary> public string MarkdownIamgeHeader { get { return this.markdownImageHeader; } } private string markdownIamgeTooltip = ""; /// <summary> /// 通过 Markdown 格式引用的图像链接的提示文本。 /// </summary> public string MarkdownImageTooltip { get { return this.markdownIamgeTooltip; } } private string markdownLinkText = ""; /// <summary> /// 通过 Markdown 格式引用的图像链接的完全文本。形如: ///  /// </summary> public string MarkdownLinkText { get { return this.markdownLinkText; } } private int orderNumber = 0; /// <summary> /// 用于给试题随机排序。 /// </summary> public int OrderNumber { get { return orderNumber; } set { this.orderNumber = value; } } } /// <summary> /// 支持的试题类别。 /// </summary> public enum QuestionType { /// <summary> /// 选择题。 /// </summary> Choice, /// <summary> /// 填空题 /// </summary> FillBlank, /// <summary> /// 判断题。 /// </summary> Judge, /// <summary> /// 主观题。 /// </summary> Subjective, /// <summary> /// 非试题,只是文本。 /// </summary> Text } }