/// <summary> /// Renders a yaml header element. /// </summary> protected override void RenderYamlHeader(YamlHeaderBlock element, IRenderContext context) { var localContext = context as UIElementCollectionRenderContext; if (localContext == null) { throw new RenderContextIncorrectException(); } var blockUIElementCollection = localContext.BlockUIElementCollection; var table = new MarkdownTable(element.Children.Count, 2, YamlBorderThickness, YamlBorderBrush) { HorizontalAlignment = HorizontalAlignment.Left, Margin = TableMargin }; // Split key and value string[] childrenKeys = new string[element.Children.Count]; string[] childrenValues = new string[element.Children.Count]; element.Children.Keys.CopyTo(childrenKeys, 0); element.Children.Values.CopyTo(childrenValues, 0); // Add each column for (int i = 0; i < element.Children.Count; i++) { // Add each cell var keyCell = new TextBlock { Text = childrenKeys[i], Foreground = Foreground, TextAlignment = TextAlignment.Center, FontWeight = FontWeights.Bold, Margin = TableCellPadding }; var valueCell = new TextBlock { Text = childrenValues[i], Foreground = Foreground, TextAlignment = TextAlignment.Left, Margin = TableCellPadding, TextWrapping = TextWrapping.Wrap }; Grid.SetRow(keyCell, 0); Grid.SetColumn(keyCell, i); Grid.SetRow(valueCell, 1); Grid.SetColumn(valueCell, i); table.Children.Add(keyCell); table.Children.Add(valueCell); } blockUIElementCollection.Add(table); }
/// <summary> /// Parses a markdown document. /// </summary> /// <param name="markdown"> The markdown text. </param> /// <param name="start"> The position to start parsing. </param> /// <param name="end"> The position to stop parsing. </param> /// <param name="quoteDepth"> The current nesting level for block quoting. </param> /// <param name="actualEnd"> Set to the position at which parsing ended. This can be /// different from <paramref name="end"/> when the parser is being called recursively. /// </param> /// <returns> A list of parsed blocks. </returns> internal static List <MarkdownBlock> Parse(string markdown, int start, int end, int quoteDepth, out int actualEnd) { // We need to parse out the list of blocks. // Some blocks need to start on a new paragraph (code, lists and tables) while other // blocks can start on any line (headers, horizontal rules and quotes). // Text that is outside of any other block becomes a paragraph. var blocks = new List <MarkdownBlock>(); int startOfLine = start; bool lineStartsNewParagraph = true; var paragraphText = new StringBuilder(); // These are needed to parse underline-style header blocks. int previousRealtStartOfLine = start; int previousStartOfLine = start; int previousEndOfLine = start; // Go line by line. while (startOfLine < end) { // Find the first non-whitespace character. int nonSpacePos = startOfLine; char nonSpaceChar = '\0'; int realStartOfLine = startOfLine; // i.e. including quotes. int expectedQuotesRemaining = quoteDepth; while (true) { while (nonSpacePos < end) { char c = markdown[nonSpacePos]; if (c == '\r' || c == '\n') { // The line is either entirely whitespace, or is empty. break; } if (c != ' ' && c != '\t') { // The line has content. nonSpaceChar = c; break; } nonSpacePos++; } // When parsing blocks in a blockquote context, we need to count the number of // quote characters ('>'). If there are less than expected AND this is the // start of a new paragraph, then stop parsing. if (expectedQuotesRemaining == 0) { break; } if (nonSpaceChar == '>') { // Expected block quote characters should be ignored. expectedQuotesRemaining--; nonSpacePos++; nonSpaceChar = '\0'; startOfLine = nonSpacePos; // Ignore the first space after the quote character, if there is one. if (startOfLine < end && markdown[startOfLine] == ' ') { startOfLine++; nonSpacePos++; } } else { int lastIndentation = 0; string lastline = null; // Determines how many Quote levels were in the last line. if (realStartOfLine > 0) { lastline = markdown.Substring(previousRealtStartOfLine, previousEndOfLine - previousRealtStartOfLine); lastIndentation = lastline.Count(c => c == '>'); } var currentEndOfLine = Common.FindNextSingleNewLine(markdown, nonSpacePos, end, out _); var currentline = markdown.Substring(realStartOfLine, currentEndOfLine - realStartOfLine); var currentIndentation = currentline.Count(c => c == '>'); var firstChar = markdown[realStartOfLine]; // This is a quote that doesn't start with a Quote marker, but carries on from the last line. if (lastIndentation == 1) { if (nonSpaceChar != '\0' && firstChar != '>') { break; } } // Collapse down a level of quotes if the current indentation is greater than the last indentation. // Only if the last indentation is greater than 1, and the current indentation is greater than 0 if (lastIndentation > 1 && currentIndentation > 0 && currentIndentation < lastIndentation) { break; } // This must be the end of the blockquote. End the current paragraph, if any. actualEnd = realStartOfLine; if (paragraphText.Length > 0) { blocks.Add(ParagraphBlock.Parse(paragraphText.ToString())); } return(blocks); } } // Find the end of the current line. int endOfLine = Common.FindNextSingleNewLine(markdown, nonSpacePos, end, out int startOfNextLine); if (nonSpaceChar == '\0') { // The line is empty or nothing but whitespace. lineStartsNewParagraph = true; // End the current paragraph. if (paragraphText.Length > 0) { blocks.Add(ParagraphBlock.Parse(paragraphText.ToString())); paragraphText.Clear(); } } else { // This is a header if the line starts with a hash character, // or if the line starts with '-' or a '=' character and has no other characters. // Or a quote if the line starts with a greater than character (optionally preceded by whitespace). // Or a horizontal rule if the line contains nothing but 3 '*', '-' or '_' characters (with optional whitespace). MarkdownBlock newBlockElement = null; if (nonSpaceChar == '-' && nonSpacePos == startOfLine) { // Yaml Header newBlockElement = YamlHeaderBlock.Parse(markdown, startOfLine, markdown.Length, out startOfLine); if (newBlockElement != null) { realStartOfLine = startOfLine; endOfLine = startOfLine + 3; startOfNextLine = Common.FindNextSingleNewLine(markdown, startOfLine, end, out startOfNextLine); paragraphText.Clear(); } } if (newBlockElement == null && nonSpaceChar == '#' && nonSpacePos == startOfLine) { // Hash-prefixed header. newBlockElement = HeaderBlock.ParseHashPrefixedHeader(markdown, startOfLine, endOfLine); } else if ((nonSpaceChar == '-' || nonSpaceChar == '=') && nonSpacePos == startOfLine && paragraphText.Length > 0) { // Underline style header. These are weird because you don't know you've // got one until you've gone past it. // Note: we intentionally deviate from reddit here in that we only // recognize this type of header if the previous line is part of a // paragraph. For example if you have this, the header at the bottom is // ignored: // a|b // -|- // 1|2 // === newBlockElement = HeaderBlock.ParseUnderlineStyleHeader(markdown, previousStartOfLine, previousEndOfLine, startOfLine, endOfLine); if (newBlockElement != null) { // We're going to have to remove the header text from the pending // paragraph by prematurely ending the current paragraph. // We already made sure that there is a paragraph in progress. paragraphText.Length -= (previousEndOfLine - previousStartOfLine); } } // These characters overlap with the underline-style header - this check should go after that one. if (newBlockElement == null && (nonSpaceChar == '*' || nonSpaceChar == '-' || nonSpaceChar == '_')) { newBlockElement = HorizontalRuleBlock.Parse(markdown, startOfLine, endOfLine); } if (newBlockElement == null && lineStartsNewParagraph) { // Some block elements must start on a new paragraph (tables, lists and code). int endOfBlock = startOfNextLine; if (nonSpaceChar == '*' || nonSpaceChar == '+' || nonSpaceChar == '-' || (nonSpaceChar >= '0' && nonSpaceChar <= '9')) { newBlockElement = ListBlock.Parse(markdown, realStartOfLine, end, quoteDepth, out endOfBlock); } if (newBlockElement == null && (nonSpacePos > startOfLine || nonSpaceChar == '`')) { newBlockElement = CodeBlock.Parse(markdown, realStartOfLine, end, quoteDepth, out endOfBlock); } if (newBlockElement == null) { newBlockElement = TableBlock.Parse(markdown, realStartOfLine, endOfLine, end, quoteDepth, out endOfBlock); } if (newBlockElement != null) { startOfNextLine = endOfBlock; } } // This check needs to go after the code block check. if (newBlockElement == null && nonSpaceChar == '>') { newBlockElement = QuoteBlock.Parse(markdown, realStartOfLine, end, quoteDepth, out startOfNextLine); } // This check needs to go after the code block check. if (newBlockElement == null && nonSpaceChar == '[') { newBlockElement = LinkReferenceBlock.Parse(markdown, startOfLine, endOfLine); } // Block elements start new paragraphs. lineStartsNewParagraph = newBlockElement != null; if (newBlockElement == null) { // The line contains paragraph text. if (paragraphText.Length > 0) { // If the previous two characters were both spaces, then append a line break. if (paragraphText.Length > 2 && paragraphText[paragraphText.Length - 1] == ' ' && paragraphText[paragraphText.Length - 2] == ' ') { // Replace the two spaces with a line break. paragraphText[paragraphText.Length - 2] = '\r'; paragraphText[paragraphText.Length - 1] = '\n'; } else { paragraphText.Append(" "); } } // Add the last paragraph if we are at the end of the input text. if (startOfNextLine >= end) { if (paragraphText.Length == 0) { // Optimize for single line paragraphs. blocks.Add(ParagraphBlock.Parse(markdown.Substring(startOfLine, endOfLine - startOfLine))); } else { // Slow path. paragraphText.Append(markdown.Substring(startOfLine, endOfLine - startOfLine)); blocks.Add(ParagraphBlock.Parse(paragraphText.ToString())); } } else { paragraphText.Append(markdown.Substring(startOfLine, endOfLine - startOfLine)); } } else { // The line contained a block. End the current paragraph, if any. if (paragraphText.Length > 0) { blocks.Add(ParagraphBlock.Parse(paragraphText.ToString())); paragraphText.Clear(); } blocks.Add(newBlockElement); } } // Repeat. previousRealtStartOfLine = realStartOfLine; previousStartOfLine = startOfLine; previousEndOfLine = endOfLine; startOfLine = startOfNextLine; } actualEnd = startOfLine; return(blocks); }
/// <summary> /// Parse yaml header /// </summary> /// <param name="markdown"> The markdown text. </param> /// <param name="start"> The location of the first hash character. </param> /// <param name="end"> The location of the end of the line. </param> /// <param name="realEndIndex"> The location of the actual end of the aprse. </param> /// <returns>Parsed <see cref="YamlHeaderBlock"/> class</returns> internal static YamlHeaderBlock Parse(string markdown, int start, int end, out int realEndIndex) { // As yaml header, must be start a line with "---" // and end with a line "---" realEndIndex = start; int lineStart = start; if (end - start < 3) { return(null); } if (lineStart != 0 || markdown.Substring(start, 3) != "---") { return(null); } int startUnderlineIndex = Common.FindNextSingleNewLine(markdown, lineStart, end, out int startOfNextLine); if (startUnderlineIndex - lineStart != 3) { return(null); } bool lockedFinalUnderline = false; // if current line not contain the ": ", check it is end of parse, if not, exit // if next line is the end, exit int pos = startOfNextLine; List <string> elements = new List <string>(); while (pos < end) { int nextUnderLineIndex = Common.FindNextSingleNewLine(markdown, pos, end, out startOfNextLine); bool haveSeparator = markdown.Substring(pos, nextUnderLineIndex - pos).Contains(": ", StringComparison.Ordinal); if (haveSeparator) { elements.Add(markdown.Substring(pos, nextUnderLineIndex - pos)); } else if (end - pos >= 3 && markdown.Substring(pos, 3) == "---") { lockedFinalUnderline = true; realEndIndex = pos + 3; break; } else if (startOfNextLine == pos + 1) { pos = startOfNextLine; continue; } else { return(null); } pos = startOfNextLine; } // if not have the end, return if (!lockedFinalUnderline) { return(null); } // parse yaml header properties if (elements.Count < 1) { return(null); } var result = new YamlHeaderBlock { Children = new Dictionary <string, string>() }; foreach (var item in elements) { string[] splits = item.Split(new string[] { ": " }, StringSplitOptions.None); if (splits.Length < 2) { continue; } else { string key = splits[0]; string value = splits[1]; if (key.Trim().Length == 0) { continue; } value = string.IsNullOrEmpty(value.Trim()) ? string.Empty : value; result.Children.Add(key, value); } } if (result.Children == null) { return(null); } return(result); }