internal void FreeBlock(Block b) { m_SpareBlocks.Push(b); }
bool IsSectionHeader(Block b) { return b.blockType >= BlockType.h1 && b.blockType <= BlockType.h3; }
public Block CopyFrom(Block other) { blockType = other.blockType; buf = other.buf; contentStart = other.contentStart; contentLen = other.contentLen; lineStart = other.lineStart; lineLen = other.lineLen; return this; }
internal void AddFootnote(Block footnote) { m_Footnotes[(string)footnote.data] = footnote; }
// Scan from the current position to the end of the html section internal bool ScanHtml(Block b) { // Remember start of html int posStartPiece = this.position; // Parse a HTML tag HtmlTag openingTag = HtmlTag.Parse(this); if (openingTag == null) return false; // Closing tag? if (openingTag.closing) return false; // Safe mode? bool bHasUnsafeContent = false; if (m_markdown.SafeMode && !openingTag.IsSafe()) bHasUnsafeContent = true; HtmlTagFlags flags = openingTag.Flags; // Is it a block level tag? if ((flags & HtmlTagFlags.Block) == 0) return false; // Closed tag, hr or comment? if ((flags & HtmlTagFlags.NoClosing) != 0 || openingTag.closed) { SkipLinespace(); SkipEol(); b.contentEnd = position; b.blockType = bHasUnsafeContent ? BlockType.unsafe_html : BlockType.html; return true; } // Can it also be an inline tag? if ((flags & HtmlTagFlags.Inline) != 0) { // Yes, opening tag must be on a line by itself SkipLinespace(); if (!eol) return false; } // Head block extraction? bool bHeadBlock = m_markdown.ExtractHeadBlocks && string.Compare(openingTag.name, "head", true) == 0; int headStart = this.position; // Work out the markdown mode for this element if (!bHeadBlock && m_markdown.ExtraMode) { MarkdownInHtmlMode MarkdownMode = this.GetMarkdownMode(openingTag); if (MarkdownMode != MarkdownInHtmlMode.NA) { return this.ProcessMarkdownEnabledHtml(b, openingTag, MarkdownMode); } } List<Block> childBlocks = null; // Now capture everything up to the closing tag and put it all in a single HTML block int depth = 1; while (!eof) { // Find next angle bracket if (!Find('<')) break; // Save position of current tag int posStartCurrentTag = position; // Is it a html tag? HtmlTag tag = HtmlTag.Parse(this); if (tag == null) { // Nope, skip it SkipForward(1); continue; } // Safe mode checks if (m_markdown.SafeMode && !tag.IsSafe()) bHasUnsafeContent = true; // Ignore self closing tags if (tag.closed) continue; // Markdown enabled content? if (!bHeadBlock && !tag.closing && m_markdown.ExtraMode && !bHasUnsafeContent) { MarkdownInHtmlMode MarkdownMode = this.GetMarkdownMode(tag); if (MarkdownMode != MarkdownInHtmlMode.NA) { Block markdownBlock = this.CreateBlock(); if (this.ProcessMarkdownEnabledHtml(markdownBlock, tag, MarkdownMode)) { if (childBlocks==null) { childBlocks = new List<Block>(); } // Create a block for everything before the markdown tag if (posStartCurrentTag > posStartPiece) { Block htmlBlock = this.CreateBlock(); htmlBlock.buf = input; htmlBlock.blockType = BlockType.html; htmlBlock.contentStart = posStartPiece; htmlBlock.contentLen = posStartCurrentTag - posStartPiece; childBlocks.Add(htmlBlock); } // Add the markdown enabled child block childBlocks.Add(markdownBlock); // Remember start of the next piece posStartPiece = position; continue; } else { this.FreeBlock(markdownBlock); } } } // Same tag? if (tag.name == openingTag.name) { if (tag.closing) { depth--; if (depth == 0) { // End of tag? SkipLinespace(); SkipEol(); // If anything unsafe detected, just encode the whole block if (bHasUnsafeContent) { b.blockType = BlockType.unsafe_html; b.contentEnd = position; return true; } // Did we create any child blocks if (childBlocks != null) { // Create a block for the remainder if (position > posStartPiece) { Block htmlBlock = this.CreateBlock(); htmlBlock.buf = input; htmlBlock.blockType = BlockType.html; htmlBlock.contentStart = posStartPiece; htmlBlock.contentLen = position - posStartPiece; childBlocks.Add(htmlBlock); } // Return a composite block b.blockType = BlockType.Composite; b.contentEnd = position; b.children = childBlocks; return true; } // Extract the head block content if (bHeadBlock) { var content = this.Substring(headStart, posStartCurrentTag - headStart); m_markdown.HeadBlockContent = (m_markdown.HeadBlockContent ?? "") + content.Trim() + "\n"; b.blockType = BlockType.html; b.contentStart = position; b.contentEnd = position; b.lineStart = position; return true; } // Straight html block b.blockType = BlockType.html; b.contentEnd = position; return true; } } else { depth++; } } } // Rewind to just after the tag return false; }
internal void CollapseLines(List<Block> blocks, List<Block> lines) { // Remove trailing blank lines while (lines.Count>0 && lines.Last().blockType == BlockType.Blank) { FreeBlock(lines.Pop()); } // Quit if empty if (lines.Count == 0) { return; } // What sort of block? switch (lines[0].blockType) { case BlockType.p: { // Collapse all lines into a single paragraph var para = CreateBlock(); para.blockType = BlockType.p; para.buf = lines[0].buf; para.contentStart = lines[0].contentStart; para.contentEnd = lines.Last().contentEnd; blocks.Add(para); FreeBlocks(lines); break; } case BlockType.quote: { // Create a new quote block var quote = new Block(BlockType.quote); quote.children = new BlockProcessor(m_markdown, m_bMarkdownInHtml, BlockType.quote).Process(RenderLines(lines)); FreeBlocks(lines); blocks.Add(quote); break; } case BlockType.ol_li: case BlockType.ul_li: blocks.Add(BuildList(lines)); break; case BlockType.dd: if (blocks.Count > 0) { var prev=blocks[blocks.Count-1]; switch (prev.blockType) { case BlockType.p: prev.blockType = BlockType.dt; break; case BlockType.dd: break; default: var wrapper = CreateBlock(); wrapper.blockType = BlockType.dt; wrapper.children = new List<Block>(); wrapper.children.Add(prev); blocks.Pop(); blocks.Add(wrapper); break; } } blocks.Add(BuildDefinition(lines)); break; case BlockType.footnote: m_markdown.AddFootnote(BuildFootnote(lines)); break; case BlockType.indent: { var codeblock = new Block(BlockType.codeblock); /* if (m_markdown.FormatCodeBlockAttributes != null) { // Does the line first line look like a syntax specifier var firstline = lines[0].Content; if (firstline.StartsWith("{{") && firstline.EndsWith("}}")) { codeblock.data = firstline.Substring(2, firstline.Length - 4); lines.RemoveAt(0); } } */ codeblock.children = new List<Block>(); codeblock.children.AddRange(lines); blocks.Add(codeblock); lines.Clear(); break; } } }
internal bool ProcessMarkdownEnabledHtml(Block b, HtmlTag openingTag, MarkdownInHtmlMode mode) { // Current position is just after the opening tag // Scan until we find matching closing tag int inner_pos = position; int depth = 1; bool bHasUnsafeContent = false; while (!eof) { // Find next angle bracket if (!Find('<')) break; // Is it a html tag? int tagpos = position; HtmlTag tag = HtmlTag.Parse(this); if (tag == null) { // Nope, skip it SkipForward(1); continue; } // In markdown off mode, we need to check for unsafe tags if (m_markdown.SafeMode && mode == MarkdownInHtmlMode.Off && !bHasUnsafeContent) { if (!tag.IsSafe()) bHasUnsafeContent = true; } // Ignore self closing tags if (tag.closed) continue; // Same tag? if (tag.name == openingTag.name) { if (tag.closing) { depth--; if (depth == 0) { // End of tag? SkipLinespace(); SkipEol(); b.blockType = BlockType.HtmlTag; b.data = openingTag; b.contentEnd = position; switch (mode) { case MarkdownInHtmlMode.Span: { Block span = this.CreateBlock(); span.buf = input; span.blockType = BlockType.span; span.contentStart = inner_pos; span.contentLen = tagpos - inner_pos; b.children = new List<Block>(); b.children.Add(span); break; } case MarkdownInHtmlMode.Block: case MarkdownInHtmlMode.Deep: { // Scan the internal content var bp = new BlockProcessor(m_markdown, mode == MarkdownInHtmlMode.Deep); b.children = bp.ScanLines(input, inner_pos, tagpos - inner_pos); break; } case MarkdownInHtmlMode.Off: { if (bHasUnsafeContent) { b.blockType = BlockType.unsafe_html; b.contentEnd = position; } else { Block span = this.CreateBlock(); span.buf = input; span.blockType = BlockType.html; span.contentStart = inner_pos; span.contentLen = tagpos - inner_pos; b.children = new List<Block>(); b.children.Add(span); } break; } } return true; } } else { depth++; } } } // Missing closing tag(s). return false; }
internal void FreeBlock(Block b) { m_markdown.FreeBlock(b); }
bool ProcessFencedCodeBlock(Block b) { // Extract the fence Mark(); while (current == '~') SkipForward(1); string strFence = Extract(); // Must be at least 3 long if (strFence.Length < 3) return false; // Rest of line must be blank SkipLinespace(); if (!eol) return false; // Skip the eol and remember start of code SkipEol(); int startCode = position; // Find the end fence if (!Find(strFence)) return false; // Character before must be a eol char if (!IsLineEnd(CharAtOffset(-1))) return false; int endCode = position; // Skip the fence SkipForward(strFence.Length); // Whitespace allowed at end SkipLinespace(); if (!eol) return false; // Create the code block b.blockType = BlockType.codeblock; b.children = new List<Block>(); // Remove the trailing line end if (input[endCode - 1] == '\r' && input[endCode - 2] == '\n') endCode -= 2; else if (input[endCode - 1] == '\n' && input[endCode - 2] == '\r') endCode -= 2; else endCode--; // Create the child block with the entire content var child = CreateBlock(); child.blockType = BlockType.indent; child.buf = input; child.contentStart = startCode; child.contentEnd = endCode; b.children.Add(child); return true; }
BlockType EvaluateLine(Block b) { // Empty line? if (eol) return BlockType.Blank; // Save start of line position int line_start= position; // ## Heading ## char ch=current; if (ch == '#') { // Work out heading level int level = 1; SkipForward(1); while (current == '#') { level++; SkipForward(1); } // Limit of 6 if (level > 6) level = 6; // Skip any whitespace SkipLinespace(); // Save start position b.contentStart = position; // Jump to end SkipToEol(); // In extra mode, check for a trailing HTML ID if (m_markdown.ExtraMode && !m_markdown.SafeMode) { int end=position; string strID = Utils.StripHtmlID(input, b.contentStart, ref end); if (strID!=null) { b.data = strID; position = end; } } // Rewind over trailing hashes while (position>b.contentStart && CharAtOffset(-1) == '#') { SkipForward(-1); } // Rewind over trailing spaces while (position>b.contentStart && char.IsWhiteSpace(CharAtOffset(-1))) { SkipForward(-1); } // Create the heading block b.contentEnd = position; SkipToEol(); return BlockType.h1 + (level - 1); } // Check for entire line as - or = for setext h1 and h2 if (ch=='-' || ch=='=') { // Skip all matching characters char chType = ch; while (current==chType) { SkipForward(1); } // Trailing whitespace allowed SkipLinespace(); // If not at eol, must have found something other than setext header if (eol) { return chType == '=' ? BlockType.post_h1 : BlockType.post_h2; } position = line_start; } // MarkdownExtra Table row indicator? if (m_markdown.ExtraMode) { TableSpec spec = TableSpec.Parse(this); if (spec!=null) { b.data = spec; return BlockType.table_spec; } position = line_start; } // Fenced code blocks? if (m_markdown.ExtraMode && ch == '~') { if (ProcessFencedCodeBlock(b)) return b.blockType; // Rewind position = line_start; } // Scan the leading whitespace, remembering how many spaces and where the first tab is int tabPos = -1; int leadingSpaces = 0; while (!eol) { if (current == ' ') { if (tabPos < 0) leadingSpaces++; } else if (current == '\t') { if (tabPos < 0) tabPos = position; } else { // Something else, get out break; } SkipForward(1); } // Blank line? if (eol) { b.contentEnd = b.contentStart; return BlockType.Blank; } // 4 leading spaces? if (leadingSpaces >= 4) { b.contentStart = line_start + 4; return BlockType.indent; } // Tab in the first 4 characters? if (tabPos >= 0 && tabPos - line_start<4) { b.contentStart = tabPos + 1; return BlockType.indent; } // Treat start of line as after leading whitespace b.contentStart = position; // Get the next character ch = current; // Html block? if (ch == '<') { // Scan html block if (ScanHtml(b)) return b.blockType; // Rewind position = b.contentStart; } // Block quotes start with '>' and have one space or one tab following if (ch == '>') { // Block quote followed by space if (IsLineSpace(CharAtOffset(1))) { // Skip it and create quote block SkipForward(2); b.contentStart = position; return BlockType.quote; } SkipForward(1); b.contentStart = position; return BlockType.quote; } // Horizontal rule - a line consisting of 3 or more '-', '_' or '*' with optional spaces and nothing else if (ch == '-' || ch == '_' || ch == '*') { int count = 0; while (!eol) { char chType = current; if (current == ch) { count++; SkipForward(1); continue; } if (IsLineSpace(current)) { SkipForward(1); continue; } break; } if (eol && count >= 3) { if (m_markdown.UserBreaks) return BlockType.user_break; else return BlockType.hr; } // Rewind position = b.contentStart; } // Abbreviation definition? if (m_markdown.ExtraMode && ch == '*' && CharAtOffset(1) == '[') { SkipForward(2); SkipLinespace(); Mark(); while (!eol && current != ']') { SkipForward(1); } var abbr = Extract().Trim(); if (current == ']' && CharAtOffset(1) == ':' && !string.IsNullOrEmpty(abbr)) { SkipForward(2); SkipLinespace(); Mark(); SkipToEol(); var title = Extract(); m_markdown.AddAbbreviation(abbr, title); return BlockType.Blank; } position = b.contentStart; } // Unordered list if ((ch == '*' || ch == '+' || ch == '-') && IsLineSpace(CharAtOffset(1))) { // Skip it SkipForward(1); SkipLinespace(); b.contentStart = position; return BlockType.ul_li; } // Definition if (ch == ':' && m_markdown.ExtraMode && IsLineSpace(CharAtOffset(1))) { SkipForward(1); SkipLinespace(); b.contentStart = position; return BlockType.dd; } // Ordered list if (char.IsDigit(ch)) { // Ordered list? A line starting with one or more digits, followed by a '.' and a space or tab // Skip all digits SkipForward(1); while (char.IsDigit(current)) SkipForward(1); if (SkipChar('.') && SkipLinespace()) { b.contentStart = position; return BlockType.ol_li; } position=b.contentStart; } // Reference link definition? if (ch == '[') { // Footnote definition? if (m_markdown.ExtraMode && CharAtOffset(1) == '^') { var savepos = position; SkipForward(2); string id; if (SkipFootnoteID(out id) && SkipChar(']') && SkipChar(':')) { SkipLinespace(); b.contentStart = position; b.data = id; return BlockType.footnote; } position = savepos; } // Parse a link definition LinkDefinition l = LinkDefinition.ParseLinkDefinition(this, m_markdown.ExtraMode); if (l!=null) { m_markdown.AddLinkDefinition(l); return BlockType.Blank; } } // Nothing special return BlockType.p; }
/* * Spacing * * 1-3 spaces - Promote to indented if more spaces than original item * */ /* * BuildList - build a single <ol> or <ul> list */ private Block BuildList(List<Block> lines) { // What sort of list are we dealing with BlockType listType = lines[0].blockType; System.Diagnostics.Debug.Assert(listType == BlockType.ul_li || listType == BlockType.ol_li); // Preprocess // 1. Collapse all plain lines (ie: handle hardwrapped lines) // 2. Promote any unindented lines that have more leading space // than the original list item to indented, including leading // special chars int leadingSpace = lines[0].leadingSpaces; for (int i = 1; i < lines.Count; i++) { // Join plain paragraphs if ((lines[i].blockType == BlockType.p) && (lines[i - 1].blockType == BlockType.p || lines[i - 1].blockType == BlockType.ul_li || lines[i - 1].blockType==BlockType.ol_li)) { lines[i - 1].contentEnd = lines[i].contentEnd; FreeBlock(lines[i]); lines.RemoveAt(i); i--; continue; } if (lines[i].blockType != BlockType.indent && lines[i].blockType != BlockType.Blank) { int thisLeadingSpace = lines[i].leadingSpaces; if (thisLeadingSpace > leadingSpace) { // Change line to indented, including original leading chars // (eg: '* ', '>', '1.' etc...) lines[i].blockType = BlockType.indent; int saveend = lines[i].contentEnd; lines[i].contentStart = lines[i].lineStart + thisLeadingSpace; lines[i].contentEnd = saveend; } } } // Create the wrapping list item var List = new Block(listType == BlockType.ul_li ? BlockType.ul : BlockType.ol); List.children = new List<Block>(); // Process all lines in the range for (int i = 0; i < lines.Count; i++) { System.Diagnostics.Debug.Assert(lines[i].blockType == BlockType.ul_li || lines[i].blockType==BlockType.ol_li); // Find start of item, including leading blanks int start_of_li = i; while (start_of_li > 0 && lines[start_of_li - 1].blockType == BlockType.Blank) start_of_li--; // Find end of the item, including trailing blanks int end_of_li = i; while (end_of_li < lines.Count - 1 && lines[end_of_li + 1].blockType != BlockType.ul_li && lines[end_of_li + 1].blockType != BlockType.ol_li) end_of_li++; // Is this a simple or complex list item? if (start_of_li == end_of_li) { // It's a simple, single line item item System.Diagnostics.Debug.Assert(start_of_li == i); List.children.Add(CreateBlock().CopyFrom(lines[i])); } else { // Build a new string containing all child items bool bAnyBlanks = false; StringBuilder sb = m_markdown.GetStringBuilder(); for (int j = start_of_li; j <= end_of_li; j++) { var l = lines[j]; sb.Append(l.buf, l.contentStart, l.contentLen); sb.Append('\n'); if (lines[j].blockType == BlockType.Blank) { bAnyBlanks = true; } } // Create the item and process child blocks var item = new Block(BlockType.li); item.children = new BlockProcessor(m_markdown, m_bMarkdownInHtml, listType).Process(sb.ToString()); // If no blank lines, change all contained paragraphs to plain text if (!bAnyBlanks) { foreach (var child in item.children) { if (child.blockType == BlockType.p) { child.blockType = BlockType.span; } } } // Add the complex item List.children.Add(item); } // Continue processing from end of li i = end_of_li; } FreeBlocks(lines); lines.Clear(); // Continue processing after this item return List; }