diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..21256661 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/ApiDocumentationTester/packages.config b/ApiDocumentationTester/packages.config index fc99f50d..f827ca24 100644 --- a/ApiDocumentationTester/packages.config +++ b/ApiDocumentationTester/packages.config @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/MarkdownDeep/BlockProcessor.cs b/MarkdownDeep/BlockProcessor.cs index 1751854b..faa8f68a 100644 --- a/MarkdownDeep/BlockProcessor.cs +++ b/MarkdownDeep/BlockProcessor.cs @@ -1,1563 +1,1563 @@ -// -// MarkdownDeep - http://www.toptensoftware.com/markdowndeep -// Copyright (C) 2010-2011 Topten Software -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this product except in -// compliance with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software distributed under the License is -// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace MarkdownDeep -{ - public class BlockProcessor : StringScanner - { - public BlockProcessor(Markdown m, bool MarkdownInHtml) - { - m_markdown = m; - m_bMarkdownInHtml = MarkdownInHtml; - m_parentType = BlockType.Blank; - } - - internal BlockProcessor(Markdown m, bool MarkdownInHtml, BlockType parentType) - { - m_markdown = m; - m_bMarkdownInHtml = MarkdownInHtml; - m_parentType = parentType; - } - - internal List Process(string str) - { - return ScanLines(str); - } - - internal List ScanLines(string str) - { - // Reset string scanner - Reset(str); - return ScanLines(); - } - - internal List ScanLines(string str, int start, int len) - { - Reset(str, start, len); - return ScanLines(); - } - - internal bool StartTable(TableSpec spec, List lines) - { - // Mustn't have more than 1 preceeding line - if (lines.Count > 1) - return false; - - // Rewind, parse the header row then fast forward back to current pos - if (lines.Count == 1) - { - int savepos = position; - position = lines[0].lineStart; - spec.Headers = spec.ParseRow(this); - if (spec.Headers == null) - return false; - position = savepos; - lines.Clear(); - } - - // Parse all rows - while (true) - { - int savepos = position; - - var row=spec.ParseRow(this); - if (row!=null) - { - spec.Rows.Add(row); - continue; - } - - position = savepos; - break; - } - - return true; - } - - internal List ScanLines() - { - // The final set of blocks will be collected here - var blocks = new List(); - - // The current paragraph/list/codeblock etc will be accumulated here - // before being collapsed into a block and store in above `blocks` list - var lines = new List(); - - // Add all blocks - BlockType PrevBlockType = BlockType.unsafe_html; - while (!eof) - { - // Remember if the previous line was blank - bool bPreviousBlank = PrevBlockType == BlockType.Blank; - - // Get the next block - var b = EvaluateLine(); - PrevBlockType = b.blockType; - - // For dd blocks, we need to know if it was preceeded by a blank line - // so store that fact as the block's data. - if (b.blockType == BlockType.dd) - { - b.data = bPreviousBlank; - } - - - // SetExt header? - if (b.blockType == BlockType.post_h1 || b.blockType == BlockType.post_h2) - { - if (lines.Count > 0) - { - // Remove the previous line and collapse the current paragraph - var prevline = lines.Pop(); - CollapseLines(blocks, lines); - - // If previous line was blank, - if (prevline.blockType != BlockType.Blank) - { - // Convert the previous line to a heading and add to block list - prevline.RevertToPlain(); - prevline.blockType = b.blockType == BlockType.post_h1 ? BlockType.h1 : BlockType.h2; - blocks.Add(prevline); - continue; - } - } - - // Couldn't apply setext header to a previous line - - if (b.blockType == BlockType.post_h1) - { - // `===` gets converted to normal paragraph - b.RevertToPlain(); - lines.Add(b); - } - else - { - // `---` gets converted to hr - if (b.contentLen >= 3) - { - b.blockType = BlockType.hr; - blocks.Add(b); - } - else - { - b.RevertToPlain(); - lines.Add(b); - } - } - - continue; - } - - - // Work out the current paragraph type - BlockType currentBlockType = lines.Count > 0 ? lines[0].blockType : BlockType.Blank; - - // Starting a table? - if (b.blockType == BlockType.table_spec) - { - // Get the table spec, save position - TableSpec spec = (TableSpec)b.data; - int savepos = position; - if (!StartTable(spec, lines)) - { - // Not a table, revert the tablespec row to plain, - // fast forward back to where we were up to and continue - // on as if nothing happened - position = savepos; - b.RevertToPlain(); - } - else - { - blocks.Add(b); - continue; - } - } - - // Process this line - switch (b.blockType) - { - case BlockType.Blank: - switch (currentBlockType) - { - case BlockType.Blank: - FreeBlock(b); - break; - - case BlockType.p: - CollapseLines(blocks, lines); - FreeBlock(b); - break; - - case BlockType.quote: - case BlockType.ol_li: - case BlockType.ul_li: - case BlockType.dd: - case BlockType.footnote: - case BlockType.indent: - lines.Add(b); - break; - - default: - System.Diagnostics.Debug.Assert(false); - break; - } - break; - - case BlockType.p: - switch (currentBlockType) - { - case BlockType.Blank: - case BlockType.p: - lines.Add(b); - break; - - case BlockType.quote: - case BlockType.ol_li: - case BlockType.ul_li: - case BlockType.dd: - case BlockType.footnote: - var prevline = lines.Last(); - if (prevline.blockType == BlockType.Blank) - { - CollapseLines(blocks, lines); - lines.Add(b); - } - else - { - lines.Add(b); - } - break; - - case BlockType.indent: - CollapseLines(blocks, lines); - lines.Add(b); - break; - - default: - System.Diagnostics.Debug.Assert(false); - break; - } - break; - - case BlockType.indent: - switch (currentBlockType) - { - case BlockType.Blank: - // Start a code block - lines.Add(b); - break; - - case BlockType.p: - case BlockType.quote: - var prevline = lines.Last(); - if (prevline.blockType == BlockType.Blank) - { - // Start a code block after a paragraph - CollapseLines(blocks, lines); - lines.Add(b); - } - else - { - // indented line in paragraph, just continue it - b.RevertToPlain(); - lines.Add(b); - } - break; - - - case BlockType.ol_li: - case BlockType.ul_li: - case BlockType.dd: - case BlockType.footnote: - case BlockType.indent: - lines.Add(b); - break; - - default: - System.Diagnostics.Debug.Assert(false); - break; - } - break; - - case BlockType.quote: - if (currentBlockType != BlockType.quote) - { - CollapseLines(blocks, lines); - } - lines.Add(b); - break; - - case BlockType.ol_li: - case BlockType.ul_li: - switch (currentBlockType) - { - case BlockType.Blank: - lines.Add(b); - break; - - case BlockType.p: - case BlockType.quote: - var prevline = lines.Last(); - if (prevline.blockType == BlockType.Blank || m_parentType==BlockType.ol_li || m_parentType==BlockType.ul_li || m_parentType==BlockType.dd) - { - // List starting after blank line after paragraph or quote - CollapseLines(blocks, lines); - lines.Add(b); - } - else - { - // List's can't start in middle of a paragraph - b.RevertToPlain(); - lines.Add(b); - } - break; - - case BlockType.ol_li: - case BlockType.ul_li: - if (b.blockType!=BlockType.ol_li && b.blockType!=BlockType.ul_li) - { - CollapseLines(blocks, lines); - } - lines.Add(b); - break; - - case BlockType.dd: - case BlockType.footnote: - if (b.blockType != currentBlockType) - { - CollapseLines(blocks, lines); - } - lines.Add(b); - break; - - case BlockType.indent: - // List after code block - CollapseLines(blocks, lines); - lines.Add(b); - break; - } - break; - - case BlockType.dd: - case BlockType.footnote: - switch (currentBlockType) - { - case BlockType.Blank: - case BlockType.p: - case BlockType.dd: - case BlockType.footnote: - CollapseLines(blocks, lines); - lines.Add(b); - break; - - default: - b.RevertToPlain(); - lines.Add(b); - break; - } - break; - - default: - CollapseLines(blocks, lines); - blocks.Add(b); - break; - } - } - - CollapseLines(blocks, lines); - - if (m_markdown.ExtraMode) - { - BuildDefinitionLists(blocks); - } - - return blocks; - } - - internal Block CreateBlock() - { - return m_markdown.CreateBlock(); - } - - internal void FreeBlock(Block b) - { - m_markdown.FreeBlock(b); - } - - internal void FreeBlocks(List blocks) - { - foreach (var b in blocks) - FreeBlock(b); - blocks.Clear(); - } - - internal string RenderLines(List lines) - { - StringBuilder b = m_markdown.GetStringBuilder(); - foreach (var l in lines) - { - b.Append(l.buf, l.contentStart, l.contentLen); - b.Append('\n'); - } - return b.ToString(); - } - - internal void CollapseLines(List blocks, List 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(); - 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(); - codeblock.children.AddRange(lines); - blocks.Add(codeblock); - lines.Clear(); - break; - } - } - } - - - Block EvaluateLine() - { - // Create a block - Block b=CreateBlock(); - - // Store line start - b.lineStart=position; - b.buf=input; - - // Scan the line - b.contentStart = position; - b.contentLen = -1; - b.blockType=EvaluateLine(b); - - // If end of line not returned, do it automatically - if (b.contentLen < 0) - { - // Move to end of line - SkipToEol(); - b.contentLen = position - b.contentStart; - } - - // Setup line length - b.lineLen=position-b.lineStart; - - // Next line - SkipEol(); - - // Create block - return b; - } - - 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 == '~' || 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; - } - - internal enum MarkdownInHtmlMode - { - NA, // No markdown attribute on the tag - Block, // markdown=1 or markdown=block - Span, // markdown=1 or markdown=span - Deep, // markdown=deep - recursive block mode - Off, // Markdown="something else" - } - - internal MarkdownInHtmlMode GetMarkdownMode(HtmlTag tag) - { - // Get the markdown attribute - string strMarkdownMode; - if (!m_markdown.ExtraMode || !tag.attributes.TryGetValue("markdown", out strMarkdownMode)) - { - if (m_bMarkdownInHtml) - return MarkdownInHtmlMode.Deep; - else - return MarkdownInHtmlMode.NA; - } - - // Remove it - tag.attributes.Remove("markdown"); - - // Parse mode - if (strMarkdownMode == "1") - return (tag.Flags & HtmlTagFlags.ContentAsSpan)!=0 ? MarkdownInHtmlMode.Span : MarkdownInHtmlMode.Block; - - if (strMarkdownMode == "block") - return MarkdownInHtmlMode.Block; - - if (strMarkdownMode == "deep") - return MarkdownInHtmlMode.Deep; - - if (strMarkdownMode == "span") - return MarkdownInHtmlMode.Span; - - return MarkdownInHtmlMode.Off; - } - - 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(); - 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(); - b.children.Add(span); - } - break; - } - } - - - return true; - } - } - else - { - depth++; - } - } - } - - // Missing closing tag(s). - return false; - } - - // 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 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(); - } - - // 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; - } - - /* - * Spacing - * - * 1-3 spaces - Promote to indented if more spaces than original item - * - */ - - /* - * BuildList - build a single
    or
      list - */ - private Block BuildList(List 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(); - - // 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; - } - - /* - * BuildDefinition - build a single
      item - */ - private Block BuildDefinition(List lines) - { - // Collapse all plain lines (ie: handle hardwrapped lines) - 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.dd)) - { - lines[i - 1].contentEnd = lines[i].contentEnd; - FreeBlock(lines[i]); - lines.RemoveAt(i); - i--; - continue; - } - } - - // Single line definition - bool bPreceededByBlank=(bool)lines[0].data; - if (lines.Count==1 && !bPreceededByBlank) - { - var ret=lines[0]; - lines.Clear(); - return ret; - } - - // Build a new string containing all child items - StringBuilder sb = m_markdown.GetStringBuilder(); - for (int i = 0; i < lines.Count; i++) - { - var l = lines[i]; - sb.Append(l.buf, l.contentStart, l.contentLen); - sb.Append('\n'); - } - - // Create the item and process child blocks - var item = this.CreateBlock(); - item.blockType = BlockType.dd; - item.children = new BlockProcessor(m_markdown, m_bMarkdownInHtml, BlockType.dd).Process(sb.ToString()); - - FreeBlocks(lines); - lines.Clear(); - - // Continue processing after this item - return item; - } - - void BuildDefinitionLists(List blocks) - { - Block currentList = null; - for (int i = 0; i < blocks.Count; i++) - { - switch (blocks[i].blockType) - { - case BlockType.dt: - case BlockType.dd: - if (currentList==null) - { - currentList=CreateBlock(); - currentList.blockType=BlockType.dl; - currentList.children=new List(); - blocks.Insert(i, currentList); - i++; - } - - currentList.children.Add(blocks[i]); - blocks.RemoveAt(i); - i--; - break; - - default: - currentList = null; - break; - } - } - } - - private Block BuildFootnote(List lines) - { - // Collapse all plain lines (ie: handle hardwrapped lines) - 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.footnote)) - { - lines[i - 1].contentEnd = lines[i].contentEnd; - FreeBlock(lines[i]); - lines.RemoveAt(i); - i--; - continue; - } - } - - // Build a new string containing all child items - StringBuilder sb = m_markdown.GetStringBuilder(); - for (int i = 0; i < lines.Count; i++) - { - var l = lines[i]; - sb.Append(l.buf, l.contentStart, l.contentLen); - sb.Append('\n'); - } - - // Create the item and process child blocks - var item = this.CreateBlock(); - item.blockType = BlockType.footnote; - item.data = lines[0].data; - item.children = new BlockProcessor(m_markdown, m_bMarkdownInHtml, BlockType.footnote).Process(sb.ToString()); - - FreeBlocks(lines); - lines.Clear(); - - // Continue processing after this item - return item; - } - - bool ProcessFencedCodeBlock(Block b) - { - char delim = current; - - // Extract the fence - Mark(); - while (current == delim) - 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) - { - // Look for a language specifier - Mark(); - while (char.IsLetterOrDigit(current)) - { - SkipForward(1); - } - string codeblockLangauge = Extract(); - b.CodeLanguage = codeblockLangauge; - //return false; - - SkipLinespace(); - } - - // 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(); - - // 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; - } - - Markdown m_markdown; - BlockType m_parentType; - bool m_bMarkdownInHtml; - } -} +// +// MarkdownDeep - http://www.toptensoftware.com/markdowndeep +// Copyright (C) 2010-2011 Topten Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this product except in +// compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is +// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MarkdownDeep +{ + public class BlockProcessor : StringScanner + { + public BlockProcessor(Markdown m, bool MarkdownInHtml) + { + m_markdown = m; + m_bMarkdownInHtml = MarkdownInHtml; + m_parentType = BlockType.Blank; + } + + internal BlockProcessor(Markdown m, bool MarkdownInHtml, BlockType parentType) + { + m_markdown = m; + m_bMarkdownInHtml = MarkdownInHtml; + m_parentType = parentType; + } + + internal List Process(string str) + { + return ScanLines(str); + } + + internal List ScanLines(string str) + { + // Reset string scanner + Reset(str); + return ScanLines(); + } + + internal List ScanLines(string str, int start, int len) + { + Reset(str, start, len); + return ScanLines(); + } + + internal bool StartTable(TableSpec spec, List lines) + { + // Mustn't have more than 1 preceeding line + if (lines.Count > 1) + return false; + + // Rewind, parse the header row then fast forward back to current pos + if (lines.Count == 1) + { + int savepos = position; + position = lines[0].lineStart; + spec.Headers = spec.ParseRow(this); + if (spec.Headers == null) + return false; + position = savepos; + lines.Clear(); + } + + // Parse all rows + while (true) + { + int savepos = position; + + var row=spec.ParseRow(this); + if (row!=null) + { + spec.Rows.Add(row); + continue; + } + + position = savepos; + break; + } + + return true; + } + + internal List ScanLines() + { + // The final set of blocks will be collected here + var blocks = new List(); + + // The current paragraph/list/codeblock etc will be accumulated here + // before being collapsed into a block and store in above `blocks` list + var lines = new List(); + + // Add all blocks + BlockType PrevBlockType = BlockType.unsafe_html; + while (!eof) + { + // Remember if the previous line was blank + bool bPreviousBlank = PrevBlockType == BlockType.Blank; + + // Get the next block + var b = EvaluateLine(); + PrevBlockType = b.blockType; + + // For dd blocks, we need to know if it was preceeded by a blank line + // so store that fact as the block's data. + if (b.blockType == BlockType.dd) + { + b.data = bPreviousBlank; + } + + + // SetExt header? + if (b.blockType == BlockType.post_h1 || b.blockType == BlockType.post_h2) + { + if (lines.Count > 0) + { + // Remove the previous line and collapse the current paragraph + var prevline = lines.Pop(); + CollapseLines(blocks, lines); + + // If previous line was blank, + if (prevline.blockType != BlockType.Blank) + { + // Convert the previous line to a heading and add to block list + prevline.RevertToPlain(); + prevline.blockType = b.blockType == BlockType.post_h1 ? BlockType.h1 : BlockType.h2; + blocks.Add(prevline); + continue; + } + } + + // Couldn't apply setext header to a previous line + + if (b.blockType == BlockType.post_h1) + { + // `===` gets converted to normal paragraph + b.RevertToPlain(); + lines.Add(b); + } + else + { + // `---` gets converted to hr + if (b.contentLen >= 3) + { + b.blockType = BlockType.hr; + blocks.Add(b); + } + else + { + b.RevertToPlain(); + lines.Add(b); + } + } + + continue; + } + + + // Work out the current paragraph type + BlockType currentBlockType = lines.Count > 0 ? lines[0].blockType : BlockType.Blank; + + // Starting a table? + if (b.blockType == BlockType.table_spec) + { + // Get the table spec, save position + TableSpec spec = (TableSpec)b.data; + int savepos = position; + if (!StartTable(spec, lines)) + { + // Not a table, revert the tablespec row to plain, + // fast forward back to where we were up to and continue + // on as if nothing happened + position = savepos; + b.RevertToPlain(); + } + else + { + blocks.Add(b); + continue; + } + } + + // Process this line + switch (b.blockType) + { + case BlockType.Blank: + switch (currentBlockType) + { + case BlockType.Blank: + FreeBlock(b); + break; + + case BlockType.p: + CollapseLines(blocks, lines); + FreeBlock(b); + break; + + case BlockType.quote: + case BlockType.ol_li: + case BlockType.ul_li: + case BlockType.dd: + case BlockType.footnote: + case BlockType.indent: + lines.Add(b); + break; + + default: + System.Diagnostics.Debug.Assert(false); + break; + } + break; + + case BlockType.p: + switch (currentBlockType) + { + case BlockType.Blank: + case BlockType.p: + lines.Add(b); + break; + + case BlockType.quote: + case BlockType.ol_li: + case BlockType.ul_li: + case BlockType.dd: + case BlockType.footnote: + var prevline = lines.Last(); + if (prevline.blockType == BlockType.Blank) + { + CollapseLines(blocks, lines); + lines.Add(b); + } + else + { + lines.Add(b); + } + break; + + case BlockType.indent: + CollapseLines(blocks, lines); + lines.Add(b); + break; + + default: + System.Diagnostics.Debug.Assert(false); + break; + } + break; + + case BlockType.indent: + switch (currentBlockType) + { + case BlockType.Blank: + // Start a code block + lines.Add(b); + break; + + case BlockType.p: + case BlockType.quote: + var prevline = lines.Last(); + if (prevline.blockType == BlockType.Blank) + { + // Start a code block after a paragraph + CollapseLines(blocks, lines); + lines.Add(b); + } + else + { + // indented line in paragraph, just continue it + b.RevertToPlain(); + lines.Add(b); + } + break; + + + case BlockType.ol_li: + case BlockType.ul_li: + case BlockType.dd: + case BlockType.footnote: + case BlockType.indent: + lines.Add(b); + break; + + default: + System.Diagnostics.Debug.Assert(false); + break; + } + break; + + case BlockType.quote: + if (currentBlockType != BlockType.quote) + { + CollapseLines(blocks, lines); + } + lines.Add(b); + break; + + case BlockType.ol_li: + case BlockType.ul_li: + switch (currentBlockType) + { + case BlockType.Blank: + lines.Add(b); + break; + + case BlockType.p: + case BlockType.quote: + var prevline = lines.Last(); + if (prevline.blockType == BlockType.Blank || m_parentType==BlockType.ol_li || m_parentType==BlockType.ul_li || m_parentType==BlockType.dd) + { + // List starting after blank line after paragraph or quote + CollapseLines(blocks, lines); + lines.Add(b); + } + else + { + // List's can't start in middle of a paragraph + b.RevertToPlain(); + lines.Add(b); + } + break; + + case BlockType.ol_li: + case BlockType.ul_li: + if (b.blockType!=BlockType.ol_li && b.blockType!=BlockType.ul_li) + { + CollapseLines(blocks, lines); + } + lines.Add(b); + break; + + case BlockType.dd: + case BlockType.footnote: + if (b.blockType != currentBlockType) + { + CollapseLines(blocks, lines); + } + lines.Add(b); + break; + + case BlockType.indent: + // List after code block + CollapseLines(blocks, lines); + lines.Add(b); + break; + } + break; + + case BlockType.dd: + case BlockType.footnote: + switch (currentBlockType) + { + case BlockType.Blank: + case BlockType.p: + case BlockType.dd: + case BlockType.footnote: + CollapseLines(blocks, lines); + lines.Add(b); + break; + + default: + b.RevertToPlain(); + lines.Add(b); + break; + } + break; + + default: + CollapseLines(blocks, lines); + blocks.Add(b); + break; + } + } + + CollapseLines(blocks, lines); + + if (m_markdown.ExtraMode) + { + BuildDefinitionLists(blocks); + } + + return blocks; + } + + internal Block CreateBlock() + { + return m_markdown.CreateBlock(); + } + + internal void FreeBlock(Block b) + { + m_markdown.FreeBlock(b); + } + + internal void FreeBlocks(List blocks) + { + foreach (var b in blocks) + FreeBlock(b); + blocks.Clear(); + } + + internal string RenderLines(List lines) + { + StringBuilder b = m_markdown.GetStringBuilder(); + foreach (var l in lines) + { + b.Append(l.buf, l.contentStart, l.contentLen); + b.Append('\n'); + } + return b.ToString(); + } + + internal void CollapseLines(List blocks, List 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(); + 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(); + codeblock.children.AddRange(lines); + blocks.Add(codeblock); + lines.Clear(); + break; + } + } + } + + + Block EvaluateLine() + { + // Create a block + Block b=CreateBlock(); + + // Store line start + b.lineStart=position; + b.buf=input; + + // Scan the line + b.contentStart = position; + b.contentLen = -1; + b.blockType=EvaluateLine(b); + + // If end of line not returned, do it automatically + if (b.contentLen < 0) + { + // Move to end of line + SkipToEol(); + b.contentLen = position - b.contentStart; + } + + // Setup line length + b.lineLen=position-b.lineStart; + + // Next line + SkipEol(); + + // Create block + return b; + } + + 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 == '~' || 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; + } + + internal enum MarkdownInHtmlMode + { + NA, // No markdown attribute on the tag + Block, // markdown=1 or markdown=block + Span, // markdown=1 or markdown=span + Deep, // markdown=deep - recursive block mode + Off, // Markdown="something else" + } + + internal MarkdownInHtmlMode GetMarkdownMode(HtmlTag tag) + { + // Get the markdown attribute + string strMarkdownMode; + if (!m_markdown.ExtraMode || !tag.attributes.TryGetValue("markdown", out strMarkdownMode)) + { + if (m_bMarkdownInHtml) + return MarkdownInHtmlMode.Deep; + else + return MarkdownInHtmlMode.NA; + } + + // Remove it + tag.attributes.Remove("markdown"); + + // Parse mode + if (strMarkdownMode == "1") + return (tag.Flags & HtmlTagFlags.ContentAsSpan)!=0 ? MarkdownInHtmlMode.Span : MarkdownInHtmlMode.Block; + + if (strMarkdownMode == "block") + return MarkdownInHtmlMode.Block; + + if (strMarkdownMode == "deep") + return MarkdownInHtmlMode.Deep; + + if (strMarkdownMode == "span") + return MarkdownInHtmlMode.Span; + + return MarkdownInHtmlMode.Off; + } + + 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(); + 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(); + b.children.Add(span); + } + break; + } + } + + + return true; + } + } + else + { + depth++; + } + } + } + + // Missing closing tag(s). + return false; + } + + // 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 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(); + } + + // 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; + } + + /* + * Spacing + * + * 1-3 spaces - Promote to indented if more spaces than original item + * + */ + + /* + * BuildList - build a single
        or
          list + */ + private Block BuildList(List 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(); + + // 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; + } + + /* + * BuildDefinition - build a single
          item + */ + private Block BuildDefinition(List lines) + { + // Collapse all plain lines (ie: handle hardwrapped lines) + 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.dd)) + { + lines[i - 1].contentEnd = lines[i].contentEnd; + FreeBlock(lines[i]); + lines.RemoveAt(i); + i--; + continue; + } + } + + // Single line definition + bool bPreceededByBlank=(bool)lines[0].data; + if (lines.Count==1 && !bPreceededByBlank) + { + var ret=lines[0]; + lines.Clear(); + return ret; + } + + // Build a new string containing all child items + StringBuilder sb = m_markdown.GetStringBuilder(); + for (int i = 0; i < lines.Count; i++) + { + var l = lines[i]; + sb.Append(l.buf, l.contentStart, l.contentLen); + sb.Append('\n'); + } + + // Create the item and process child blocks + var item = this.CreateBlock(); + item.blockType = BlockType.dd; + item.children = new BlockProcessor(m_markdown, m_bMarkdownInHtml, BlockType.dd).Process(sb.ToString()); + + FreeBlocks(lines); + lines.Clear(); + + // Continue processing after this item + return item; + } + + void BuildDefinitionLists(List blocks) + { + Block currentList = null; + for (int i = 0; i < blocks.Count; i++) + { + switch (blocks[i].blockType) + { + case BlockType.dt: + case BlockType.dd: + if (currentList==null) + { + currentList=CreateBlock(); + currentList.blockType=BlockType.dl; + currentList.children=new List(); + blocks.Insert(i, currentList); + i++; + } + + currentList.children.Add(blocks[i]); + blocks.RemoveAt(i); + i--; + break; + + default: + currentList = null; + break; + } + } + } + + private Block BuildFootnote(List lines) + { + // Collapse all plain lines (ie: handle hardwrapped lines) + 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.footnote)) + { + lines[i - 1].contentEnd = lines[i].contentEnd; + FreeBlock(lines[i]); + lines.RemoveAt(i); + i--; + continue; + } + } + + // Build a new string containing all child items + StringBuilder sb = m_markdown.GetStringBuilder(); + for (int i = 0; i < lines.Count; i++) + { + var l = lines[i]; + sb.Append(l.buf, l.contentStart, l.contentLen); + sb.Append('\n'); + } + + // Create the item and process child blocks + var item = this.CreateBlock(); + item.blockType = BlockType.footnote; + item.data = lines[0].data; + item.children = new BlockProcessor(m_markdown, m_bMarkdownInHtml, BlockType.footnote).Process(sb.ToString()); + + FreeBlocks(lines); + lines.Clear(); + + // Continue processing after this item + return item; + } + + bool ProcessFencedCodeBlock(Block b) + { + char delim = current; + + // Extract the fence + Mark(); + while (current == delim) + 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) + { + // Look for a language specifier + Mark(); + while (char.IsLetterOrDigit(current)) + { + SkipForward(1); + } + string codeblockLangauge = Extract(); + b.CodeLanguage = codeblockLangauge; + //return false; + + SkipLinespace(); + } + + // 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(); + + // 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; + } + + Markdown m_markdown; + BlockType m_parentType; + bool m_bMarkdownInHtml; + } +} diff --git a/MarkdownScanner.sln b/MarkdownScanner.sln index 4ddbc4bd..557c94a2 100644 --- a/MarkdownScanner.sln +++ b/MarkdownScanner.sln @@ -1,109 +1,109 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30324.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownDeep", "MarkdownDeep\MarkdownDeep.csproj", "{1569ED47-C7C9-4261-B6F4-7445BD0F2C95}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneDrive.ApiDocumentation.Windows", "ApiDocumentationTester\OneDrive.ApiDocumentation.Windows.csproj", "{53A12F19-7C03-4E61-A34E-ADC5C211DE6E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicrosoftAccountSDK.Windows", "MicrosoftAccountSDK.Windows\MicrosoftAccountSDK.Windows.csproj", "{9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneDrive.ApiDocumentation.Validation", "OneDrive.ApiDocumentation.Validation\OneDrive.ApiDocumentation.Validation.csproj", "{33B10320-3802-49CF-8965-3510AE66D5EC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneDrive.ApiDocumentation.Console", "OneDrive.ApiDocumentation.CommandLine\OneDrive.ApiDocumentation.Console.csproj", "{A6F3993F-59C6-4985-ACF1-4D837D61E98F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneDrive.UnitTests.ApiDocumentation.Validation", "OneDrive.UnitTests.ApiDocumentation.Validation\OneDrive.UnitTests.ApiDocumentation.Validation.csproj", "{EE3453F1-FD69-406C-9BD7-0643D6E999F3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{464F5D09-2636-459F-96C9-C968FF4CA7B5}" - ProjectSection(SolutionItems) = preProject - readme.md = readme.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{8B341090-0532-4212-A3BD-A14886B39AF1}" - ProjectSection(SolutionItems) = preProject - .nuget\NuGet.Config = .nuget\NuGet.Config - .nuget\NuGet.exe = .nuget\NuGet.exe - .nuget\NuGet.targets = .nuget\NuGet.targets - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|Mixed Platforms = Debug|Mixed Platforms - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|Mixed Platforms = Release|Mixed Platforms - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|x86.ActiveCfg = Debug|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|x86.Build.0 = Debug|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|Any CPU.Build.0 = Release|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|x86.ActiveCfg = Release|Any CPU - {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|x86.Build.0 = Release|Any CPU - {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Debug|x86.ActiveCfg = Debug|Any CPU - {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Release|Any CPU.Build.0 = Release|Any CPU - {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Release|x86.ActiveCfg = Release|Any CPU - {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Debug|x86.ActiveCfg = Debug|Any CPU - {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Release|Any CPU.Build.0 = Release|Any CPU - {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Release|x86.ActiveCfg = Release|Any CPU - {33B10320-3802-49CF-8965-3510AE66D5EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {33B10320-3802-49CF-8965-3510AE66D5EC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {33B10320-3802-49CF-8965-3510AE66D5EC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {33B10320-3802-49CF-8965-3510AE66D5EC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {33B10320-3802-49CF-8965-3510AE66D5EC}.Debug|x86.ActiveCfg = Debug|Any CPU - {33B10320-3802-49CF-8965-3510AE66D5EC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {33B10320-3802-49CF-8965-3510AE66D5EC}.Release|Any CPU.Build.0 = Release|Any CPU - {33B10320-3802-49CF-8965-3510AE66D5EC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {33B10320-3802-49CF-8965-3510AE66D5EC}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {33B10320-3802-49CF-8965-3510AE66D5EC}.Release|x86.ActiveCfg = Release|Any CPU - {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Debug|x86.ActiveCfg = Debug|Any CPU - {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Release|Any CPU.Build.0 = Release|Any CPU - {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Release|x86.ActiveCfg = Release|Any CPU - {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Debug|x86.ActiveCfg = Debug|Any CPU - {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Release|Any CPU.Build.0 = Release|Any CPU - {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Release|x86.ActiveCfg = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - StartupItem = OneDrive.ApiDocumentation.CommandLine\OneDrive.ApiDocumentation.Console.csproj - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30324.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownDeep", "MarkdownDeep\MarkdownDeep.csproj", "{1569ED47-C7C9-4261-B6F4-7445BD0F2C95}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneDrive.ApiDocumentation.Windows", "ApiDocumentationTester\OneDrive.ApiDocumentation.Windows.csproj", "{53A12F19-7C03-4E61-A34E-ADC5C211DE6E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicrosoftAccountSDK.Windows", "MicrosoftAccountSDK.Windows\MicrosoftAccountSDK.Windows.csproj", "{9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneDrive.ApiDocumentation.Validation", "OneDrive.ApiDocumentation.Validation\OneDrive.ApiDocumentation.Validation.csproj", "{33B10320-3802-49CF-8965-3510AE66D5EC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneDrive.ApiDocumentation.Console", "OneDrive.ApiDocumentation.CommandLine\OneDrive.ApiDocumentation.Console.csproj", "{A6F3993F-59C6-4985-ACF1-4D837D61E98F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneDrive.UnitTests.ApiDocumentation.Validation", "OneDrive.UnitTests.ApiDocumentation.Validation\OneDrive.UnitTests.ApiDocumentation.Validation.csproj", "{EE3453F1-FD69-406C-9BD7-0643D6E999F3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{464F5D09-2636-459F-96C9-C968FF4CA7B5}" + ProjectSection(SolutionItems) = preProject + readme.md = readme.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{8B341090-0532-4212-A3BD-A14886B39AF1}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|x86.ActiveCfg = Debug|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Debug|x86.Build.0 = Debug|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|Any CPU.Build.0 = Release|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|x86.ActiveCfg = Release|Any CPU + {1569ED47-C7C9-4261-B6F4-7445BD0F2C95}.Release|x86.Build.0 = Release|Any CPU + {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Debug|x86.ActiveCfg = Debug|Any CPU + {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Release|Any CPU.Build.0 = Release|Any CPU + {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {53A12F19-7C03-4E61-A34E-ADC5C211DE6E}.Release|x86.ActiveCfg = Release|Any CPU + {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Debug|x86.ActiveCfg = Debug|Any CPU + {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Release|Any CPU.Build.0 = Release|Any CPU + {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {9E9F37BE-3CB5-4124-8B7C-E60C4AB90ECF}.Release|x86.ActiveCfg = Release|Any CPU + {33B10320-3802-49CF-8965-3510AE66D5EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33B10320-3802-49CF-8965-3510AE66D5EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33B10320-3802-49CF-8965-3510AE66D5EC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {33B10320-3802-49CF-8965-3510AE66D5EC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {33B10320-3802-49CF-8965-3510AE66D5EC}.Debug|x86.ActiveCfg = Debug|Any CPU + {33B10320-3802-49CF-8965-3510AE66D5EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33B10320-3802-49CF-8965-3510AE66D5EC}.Release|Any CPU.Build.0 = Release|Any CPU + {33B10320-3802-49CF-8965-3510AE66D5EC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {33B10320-3802-49CF-8965-3510AE66D5EC}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {33B10320-3802-49CF-8965-3510AE66D5EC}.Release|x86.ActiveCfg = Release|Any CPU + {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Release|Any CPU.Build.0 = Release|Any CPU + {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {A6F3993F-59C6-4985-ACF1-4D837D61E98F}.Release|x86.ActiveCfg = Release|Any CPU + {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Debug|x86.ActiveCfg = Debug|Any CPU + {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Release|Any CPU.Build.0 = Release|Any CPU + {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {EE3453F1-FD69-406C-9BD7-0643D6E999F3}.Release|x86.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + StartupItem = OneDrive.ApiDocumentation.CommandLine\OneDrive.ApiDocumentation.Console.csproj + EndGlobalSection +EndGlobal diff --git a/OneDrive.ApiDocumentation.CommandLine/Program.cs b/OneDrive.ApiDocumentation.CommandLine/Program.cs index 6903780f..88833056 100644 --- a/OneDrive.ApiDocumentation.CommandLine/Program.cs +++ b/OneDrive.ApiDocumentation.CommandLine/Program.cs @@ -1,754 +1,754 @@ -using Newtonsoft.Json; -using OneDrive.ApiDocumentation.Validation; -using OneDrive.ApiDocumentation.Validation.Http; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OneDrive.ApiDocumentation.ConsoleApp -{ - class Program - { - private const int ExitCodeFailure = 1; - private const int ExitCodeSuccess = 0; - - private const ConsoleColor ConsoleDefaultColor = ConsoleColor.White; - private const ConsoleColor ConsoleHeaderColor = ConsoleColor.Cyan; - private const ConsoleColor ConsoleSubheaderColor = ConsoleColor.DarkCyan; - private const ConsoleColor ConsoleCodeColor = ConsoleColor.Gray; - private const ConsoleColor ConsoleErrorColor = ConsoleColor.Red; - private const ConsoleColor ConsoleWarningColor = ConsoleColor.Yellow; - private const ConsoleColor ConsoleSuccessColor = ConsoleColor.Green; - - static void Main(string[] args) - { - string verbName = null; - BaseOptions verbOptions = null; - - var options = new CommandLineOptions(); - if (!CommandLine.Parser.Default.ParseArguments(args, options, - (verb, subOptions) => - { - // if parsing succeeds the verb name and correct instance - // will be passed to onVerbCommand delegate (string,object) - verbName = verb; - verbOptions = (BaseOptions)subOptions; - })) - { - Exit(failure: true); - } - - var commandOptions = verbOptions as DocSetOptions; - if (null != commandOptions) - { - FancyConsole.WriteVerboseOutput = commandOptions.Verbose; - } - - FancyConsole.LogFileName = verbOptions.LogFile; - -#if DEBUG - System.Diagnostics.Debugger.Launch(); -#endif - - - Nito.AsyncEx.AsyncContext.Run(() => RunInvokedMethodAsync(options, verbName, verbOptions)); - } - - private static async Task RunInvokedMethodAsync(CommandLineOptions origCommandLineOpts, string invokedVerb, BaseOptions options) - { - string[] missingProps; - if (!options.HasRequiredProperties(out missingProps)) - { - if (options is SetCommandOptions) - { - // Just print out the current values of the set parameters - FancyConsole.WriteLine(origCommandLineOpts.GetUsage(invokedVerb)); - FancyConsole.WriteLine(); - WriteSavedValues(SavedSettings.Default); - Exit(failure: true); - } - var error = new ValidationError(ValidationErrorCode.MissingRequiredArguments, null, "Command line is missing required arguments: {0}", missingProps.ComponentsJoinedByString(", ")); - FancyConsole.WriteLine(origCommandLineOpts.GetUsage(invokedVerb)); - WriteOutErrors(new ValidationError[] { error }); - Exit(failure: true); - } - - switch (invokedVerb) - { - case CommandLineOptions.VerbPrint: - PrintDocInformation((PrintOptions)options); - break; - case CommandLineOptions.VerbCheckLinks: - VerifyLinks((DocSetOptions)options); - break; - case CommandLineOptions.VerbDocs: - CheckMethodExamples((ConsistencyCheckOptions)options); - break; - case CommandLineOptions.VerbService: - await CheckMethodsAgainstService((ServiceConsistencyOptions)options); - break; - case CommandLineOptions.VerbSet: - SetDefaultValues((SetCommandOptions)options); - break; - case CommandLineOptions.VerbClean: - await PublishDocumentationAsync((PublishOptions)options); - break; - case CommandLineOptions.VerbMetadata: - await CheckServiceMetadata((CheckMetadataOptions)options); - break; - } - } - - - private static void SetDefaultValues(SetCommandOptions setCommandOptions) - { - var settings = SavedSettings.Default; - if (setCommandOptions.ResetStoredValues) - { - settings.AccessToken = null; - settings.DocumentationPath = null; - settings.ServiceUrl = null; - } - - bool setValues = false; - - if (!string.IsNullOrEmpty(setCommandOptions.AccessToken)) - { - settings.AccessToken = setCommandOptions.AccessToken; - setValues = true; - } - - if (!string.IsNullOrEmpty(setCommandOptions.DocumentationPath)) - { - settings.DocumentationPath = setCommandOptions.DocumentationPath; - setValues = true; - } - - if (!string.IsNullOrEmpty(setCommandOptions.ServiceUrl)) - { - settings.ServiceUrl = setCommandOptions.ServiceUrl; - setValues = true; - } - - settings.Save(); - - if (setCommandOptions.PrintValues || setValues) - { - WriteSavedValues(settings); - } - - } - - private static void WriteSavedValues(SavedSettings settings) - { - FancyConsole.WriteLine(ConsoleHeaderColor, "Stored settings:"); - FancyConsole.WriteLineIndented(" ", "{0}: {1}", "AccessToken", settings.AccessToken); - FancyConsole.WriteLineIndented(" ", "{0}: {1}", "DocumentationPath", settings.DocumentationPath); - FancyConsole.WriteLineIndented(" ", "{0}: {1}", "ServiceUrl", settings.ServiceUrl); - } - - - /// - /// Create a document set based on input options - /// - /// - /// - private static DocSet GetDocSet(DocSetOptions options) - { - FancyConsole.VerboseWriteLine("Opening documentation from {0}", options.PathToDocSet); - DocSet set = new DocSet(options.PathToDocSet); - - FancyConsole.VerboseWriteLine("Scanning documentation files..."); - ValidationError[] loadErrors; - if (!set.ScanDocumentation(out loadErrors) && options.ShowLoadWarnings) - { - WriteOutErrors(loadErrors); - } - - var serviceOptions = options as ServiceConsistencyOptions; - if (null != serviceOptions) - { - FancyConsole.VerboseWriteLine("Reading configuration parameters..."); - set.LoadTestScenarios(serviceOptions.ScenarioFilePath); - if (!set.TestScenarios.Loaded) - { - FancyConsole.WriteLine(ConsoleWarningColor, "Unable to read request parameter configuration file: {0}", serviceOptions.ScenarioFilePath); - } - } - - return set; - } - - private static void PrintDocInformation(PrintOptions options) - { - DocSet docset = GetDocSet(options); - if (options.PrintFiles) - { - PrintFiles(options, docset); - } - if (options.PrintResources) - { - PrintResources(options, docset); - } - if (options.PrintMethods) - { - PrintMethods(options, docset); - } - } - - private static void PrintFiles(DocSetOptions options, DocSet docset) - { - if (null == docset) - docset = GetDocSet(options); - - FancyConsole.WriteLine(); - FancyConsole.WriteLine(ConsoleHeaderColor, "Documentation files"); - - string format = null; - if (options.Verbose) - format = "{1} (resources: {2}, methods: {3})"; - else if (options.ShortForm) - format = "{0}"; - else - format = "{0} (r:{2}, m:{3})"; - - foreach (var file in docset.Files) - { - ConsoleColor color = ConsoleSuccessColor; - if (file.Resources.Length == 0 && file.Requests.Length == 0) - color = ConsoleWarningColor; - - FancyConsole.WriteLineIndented(" ", color, format, file.DisplayName, file.FullPath, file.Resources.Length, file.Requests.Length); - } - } - - private static void VerifyLinks(DocSetOptions options) - { - var docset = GetDocSet(options); - ValidationError[] errors; - docset.ValidateLinks(options.Verbose, out errors); - - if (null != errors && errors.Length > 0) - { - WriteOutErrors(errors); - if (errors.All(x => !x.IsWarning && !x.IsError)) - { - FancyConsole.WriteLine(ConsoleSuccessColor, "No link errors detected."); - Exit(failure: false); - } - } - else - { - FancyConsole.WriteLine(ConsoleSuccessColor, "No link errors detected."); - Exit(failure: false); - } - } - - private static void PrintResources(DocSetOptions options, DocSet docset) - { - if (null == docset) - docset = GetDocSet(options); - - FancyConsole.WriteLine(); - FancyConsole.WriteLine(ConsoleHeaderColor, "Defined resources:"); - FancyConsole.WriteLine(); - - var sortedResources = docset.Resources.OrderBy(x => x.ResourceType); - - foreach (var resource in sortedResources) - { - - - if (!options.ShortForm && options.Verbose) - { - string metadata = JsonConvert.SerializeObject(resource.Metadata); - FancyConsole.Write(" "); - FancyConsole.Write(ConsoleHeaderColor, resource.ResourceType); - FancyConsole.WriteLine(" flags: {1}", resource.ResourceType, metadata); - } - else - { - FancyConsole.WriteLineIndented(" ", resource.ResourceType); - } - - if (!options.ShortForm) - { - FancyConsole.WriteLineIndented(" ", ConsoleCodeColor, resource.JsonExample); - FancyConsole.WriteLine(); - } - } - } - - private static void PrintMethods(DocSetOptions options, DocSet docset) - { - if (null == docset) - docset = GetDocSet(options); - - FancyConsole.WriteLine(); - FancyConsole.WriteLine(ConsoleHeaderColor, "Defined methods:"); - FancyConsole.WriteLine(); - - foreach (var method in docset.Methods) - { - FancyConsole.WriteLine(ConsoleHeaderColor, "Method '{0}' in file '{1}'", method.DisplayName, method.SourceFile.DisplayName); - - if (!options.ShortForm) - { - var requestMetadata = options.Verbose ? JsonConvert.SerializeObject(method.RequestMetadata) : string.Empty; - FancyConsole.WriteLineIndented(" ", ConsoleSubheaderColor, "Request: {0}", requestMetadata); - FancyConsole.WriteLineIndented(" ", ConsoleCodeColor, method.Request); - } - - if (options.Verbose) - { - FancyConsole.WriteLine(); - var responseMetadata = JsonConvert.SerializeObject(method.ExpectedResponseMetadata); - if (options.ShortForm) - FancyConsole.WriteLineIndented(" ", ConsoleHeaderColor, "Expected Response: {0}", method.ExpectedResponse.TopLineOnly()); - else - { - FancyConsole.WriteLineIndented(" ", ConsoleSubheaderColor, "Expected Response: {0}", responseMetadata); - FancyConsole.WriteLineIndented(" ", ConsoleCodeColor, method.ExpectedResponse); - } - FancyConsole.WriteLine(); - } - FancyConsole.WriteLine(); - } - } - - private static MethodDefinition LookUpMethod(DocSet docset, string methodName) - { - var query = from m in docset.Methods - where m.DisplayName.Equals(methodName, StringComparison.OrdinalIgnoreCase) - select m; - - return query.FirstOrDefault(); - } - - private static void CheckMethodExamples(ConsistencyCheckOptions options) - { - var docset = GetDocSet(options); - FancyConsole.WriteLine(); - - MethodDefinition[] methods = FindTestMethods(options, docset); - - bool result = true; - int successCount = 0, errorCount = 0, warningCount = 0; - foreach (var method in methods) - { - FancyConsole.Write(ConsoleHeaderColor, "Checking \"{0}\" in {1}...", method.DisplayName, method.SourceFile.DisplayName); - - if (string.IsNullOrEmpty(method.ExpectedResponse)) - { - FancyConsole.WriteLine(); - FancyConsole.WriteLine(ConsoleErrorColor, " Error: response was null."); - errorCount++; - continue; - } - - var parser = new HttpParser(); - var expectedResponse = parser.ParseHttpResponse(method.ExpectedResponse); - ValidationError[] errors = ValidateHttpResponse(docset, method, expectedResponse); - result &= errors.WereErrors(); - - if (errors.WereErrors()) - { - errorCount++; - } - else if (errors.WereWarnings()) - { - warningCount++; - } - else - { - successCount++; - } - } - - if (options.IgnoreWarnings) - { - successCount += warningCount; - warningCount = 0; - } - - PrintStatusMessage(successCount, warningCount, errorCount); - - Exit(failure: !result); - } - - private static MethodDefinition[] FindTestMethods(ConsistencyCheckOptions options, DocSet docset) - { - MethodDefinition[] methods = null; - if (!string.IsNullOrEmpty(options.MethodName)) - { - var foundMethod = LookUpMethod(docset, options.MethodName); - if (null == foundMethod) - { - FancyConsole.WriteLine(ConsoleErrorColor, "Unable to locate method '{0}' in docset.", options.MethodName); - Exit(failure: true); - } - methods = new MethodDefinition[] { LookUpMethod(docset, options.MethodName) }; - } - else if (!string.IsNullOrEmpty(options.FileName)) - { - var selectedFileQuery = from f in docset.Files where f.DisplayName == options.FileName select f; - var selectedFile = selectedFileQuery.SingleOrDefault(); - if (selectedFile == null) - { - FancyConsole.WriteLine(ConsoleErrorColor, "Unable to locate file '{0}' in docset.", options.FileName); - Exit(failure: true); - } - methods = selectedFile.Requests; - } - else - { - methods = docset.Methods; - } - return methods; - } - - private static void WriteOutErrors(IEnumerable errors, string indent = "") - { - foreach (var error in errors) - { - if (!error.IsWarning && !error.IsError && !FancyConsole.WriteVerboseOutput) - continue; - WriteValidationError(indent, error); - } - } - - private static void WriteValidationError(string indent, ValidationError error) - { - ConsoleColor color; - if (error.IsWarning) - color = ConsoleWarningColor; - else if (error.IsError) - color = ConsoleErrorColor; - else - color = ConsoleDefaultColor; - - FancyConsole.WriteLineIndented(indent, color, error.ErrorText); - } - - private static ValidationError[] ValidateHttpResponse(DocSet docset, MethodDefinition method, HttpResponse response, HttpResponse expectedResponse = null, string indentLevel = "") - { - ValidationError[] errors; - if (!docset.ValidateApiMethod(method, response, expectedResponse, out errors)) - { - FancyConsole.WriteLine(); - WriteOutErrors(errors, indentLevel + " "); - FancyConsole.WriteLine(); - } - else - { - FancyConsole.WriteLine(ConsoleSuccessColor, " no errors"); - } - return errors; - } - - /// - /// Make requests against the service. Uses DocSet.RunParameter information to alter requests. - /// - /// - /// - private static async Task CheckMethodsAgainstService(ServiceConsistencyOptions options) - { - var docset = GetDocSet(options); - FancyConsole.WriteLine(); - - var methods = FindTestMethods(options, docset); - int successCount = 0, warningCount = 0, errorCount = 0; - bool result = true; - foreach (var method in methods) - { - ValidationError[] errors = null; - - FancyConsole.Write(ConsoleHeaderColor, "Calling method \"{0}\"...", method.DisplayName); - if (method.RequestMetadata.Disabled) - { - errors = new ValidationError[] { new ValidationWarning(ValidationErrorCode.MethodDisabled, null, "Method was disabled: {0}", method.DisplayName) }; - FancyConsole.WriteLine(); - WriteOutErrors(errors, " "); - warningCount++; - continue; - } - - AuthenicationCredentials credentials = AuthenicationCredentials.CreateAutoCredentials(options.AccessToken); - var setsOfParameters = docset.TestScenarios.ScenariosForMethod(method); - if (setsOfParameters.Length == 0) - { - // If there are no parameters defined, we still try to call the request as-is. - errors = await TestMethodWithParameters(docset, method, null, options.ServiceRootUrl, credentials); - if (errors.WereErrors()) - { - errorCount++; - } - else if (errors.WereWarnings()) - { - warningCount++; - } - else - { - successCount++; - } - AddPause(options); - } - else - { - // Otherwise, if there are parameter sets, we call each of them and check the result. - foreach (var requestSettings in setsOfParameters.Where(s => s.Enabled)) - { - errors = await TestMethodWithParameters(docset, method, requestSettings, options.ServiceRootUrl, credentials); - if (errors.WereErrors()) - { - errorCount++; - } - else if (errors.WereWarnings()) - { - warningCount++; - } - else - { - successCount++; - } - AddPause(options); - } - } - - FancyConsole.WriteLine(); - } - - if (options.IgnoreWarnings) - { - successCount += warningCount; - warningCount = 0; - } - - PrintStatusMessage(successCount, warningCount, errorCount); - result = (errorCount > 0) || (warningCount > 0); - Exit(!result); - } - - private static void Exit(bool failure) - { -#if DEBUG - if (System.Diagnostics.Debugger.IsAttached) - { - Console.WriteLine(); - Console.Write("Press any key to exit."); - Console.ReadKey(); - } -#endif - - Environment.Exit(failure ? ExitCodeFailure : ExitCodeSuccess); - } - - - private static void PrintStatusMessage(int successCount, int warningCount, int errorCount) - { - FancyConsole.WriteLine(); - FancyConsole.Write("Runs completed. "); - var totalCount = successCount + warningCount + errorCount; - double percentSuccessful = 100 * (successCount / (double)totalCount); - - const string percentCompleteFormat = "{0:0.00}% passed"; - if (percentSuccessful == 100.0) - FancyConsole.Write(ConsoleSuccessColor, percentCompleteFormat, percentSuccessful); - else - FancyConsole.Write(ConsoleWarningColor, percentCompleteFormat, percentSuccessful); - - if (errorCount > 0 || warningCount > 0) - { - FancyConsole.Write(" ("); - if (errorCount > 0) - FancyConsole.Write(ConsoleErrorColor, "{0} errors", errorCount); - if (warningCount > 0 && errorCount > 0) - FancyConsole.Write(", "); - if (warningCount > 0) - FancyConsole.Write(ConsoleWarningColor, "{0} warnings", warningCount); - if (warningCount > 0 || errorCount > 0 && successCount > 0) - FancyConsole.Write(", "); - if (successCount > 0) - FancyConsole.Write(ConsoleSuccessColor, "{0} successful", successCount); - FancyConsole.Write(")"); - } - FancyConsole.WriteLine(); - } - - private static void AddPause(ServiceConsistencyOptions options) - { - if (options.PauseBetweenRequests) - { - FancyConsole.Write("Press any key to continue"); - Console.ReadKey(); - FancyConsole.WriteLine(); - } - } - - private static async Task TestMethodWithParameters(DocSet docset, MethodDefinition method, ScenarioDefinition requestSettings, string rootUrl, AuthenicationCredentials credentials) - { - string indentLevel = ""; - if (requestSettings != null) - { - FancyConsole.WriteLine(); - FancyConsole.Write(ConsoleHeaderColor, " With scenario \"{1}\"...", method.DisplayName, requestSettings.Description); - indentLevel = " "; - } - - FancyConsole.VerboseWriteLine(""); - FancyConsole.VerboseWriteLineIndented(indentLevel, "Request:"); - var requestPreviewResult = await method.PreviewRequestAsync(requestSettings, rootUrl, credentials); - if (requestPreviewResult.IsWarningOrError) - { - WriteOutErrors(requestPreviewResult.Messages, indentLevel + " "); - return requestPreviewResult.Messages; - } - - var requestPreview = requestPreviewResult.Value; - FancyConsole.VerboseWriteLineIndented(indentLevel + " ", requestPreview.FullHttpText()); - - var parser = new HttpParser(); - var expectedResponse = parser.ParseHttpResponse(method.ExpectedResponse); - - var request = requestPreview.PrepareHttpWebRequest(rootUrl); - var actualResponse = await HttpResponse.ResponseFromHttpWebResponseAsync(request); - - FancyConsole.VerboseWriteLineIndented(indentLevel, "Response:"); - FancyConsole.VerboseWriteLineIndented(indentLevel + " ", actualResponse.FullHttpText()); - FancyConsole.VerboseWriteLine(); - - FancyConsole.VerboseWriteLineIndented(indentLevel, "Validation results:"); - return ValidateHttpResponse(docset, method, actualResponse, expectedResponse, indentLevel); - } - - private static async Task PublishDocumentationAsync(PublishOptions options) - { - var outputPath = options.OutputDirectory; - var inputPath = options.PathToDocSet; - - FancyConsole.WriteLine("Publishing documentation to {0}", outputPath); - - DocSet docs = GetDocSet(options); - DocumentPublisher publisher = null; - switch (options.Format) - { - case PublishOptions.SanitizedFormat.Markdown: - publisher = new DocumentPublisher(docs); - break; - case PublishOptions.SanitizedFormat.Html: - publisher = new DocumentPublisherHtml(docs); - break; - case PublishOptions.SanitizedFormat.Swagger2: - publisher = new OneDrive.ApiDocumentation.Validation.Writers.SwaggerWriter(docs); - break; - default: - throw new NotSupportedException("Unsupported format: " + options.Format.ToString()); - } - - publisher.VerboseLogging = options.Verbose; - publisher.SourceFileExtensions = options.TextFileExtensions; - FancyConsole.WriteLineIndented(" ", "File extensions: {0}", publisher.SourceFileExtensions); - - if (!string.IsNullOrEmpty(options.IgnorePaths)) - publisher.SkipPaths = options.IgnorePaths; - FancyConsole.WriteLineIndented(" ", "Ignored Paths: {0}", publisher.SkipPaths); - publisher.PublishAllFiles = options.PublishAllFiles; - FancyConsole.WriteLineIndented(" ", "Include all files: {0}", publisher.PublishAllFiles); - FancyConsole.WriteLine(); - - FancyConsole.WriteLine("Publishing content..."); - publisher.NewMessage += publisher_NewMessage; - await publisher.PublishToFolderAsync(outputPath); - - FancyConsole.WriteLine(ConsoleSuccessColor, "Finished publishing documentation to: {0}", outputPath); - } - - static void publisher_NewMessage(object sender, ValidationError e) - { - if (!FancyConsole.WriteVerboseOutput && !e.IsError && !e.IsWarning) - return; - - WriteValidationError("", e); - } - - private static async Task CheckServiceMetadata(CheckMetadataOptions options) - { - if (string.IsNullOrEmpty(options.ServiceMetadataLocation)) - { - if (!string.IsNullOrEmpty(SavedSettings.Default.ServiceUrl)) - { - options.ServiceMetadataLocation = SavedSettings.Default.ServiceUrl + "$metadata"; - } - else - { - FancyConsole.WriteLine(ConsoleErrorColor, "No service metadata file location specified. Cannot continue."); - Exit(failure: true); - } - } - - FancyConsole.WriteLine(ConsoleHeaderColor, "Loading service metadata from '{0}'...", options.ServiceMetadataLocation); - - Uri metadataUrl; - List schemas = null; - try - { - if (Uri.TryCreate(options.ServiceMetadataLocation, UriKind.Absolute, out metadataUrl)) - { - schemas = await OneDrive.ApiDocumentation.Validation.OData.ODataParser.ReadSchemaFromMetadataUrl(metadataUrl); - } - else - { - schemas = await OneDrive.ApiDocumentation.Validation.OData.ODataParser.ReadSchemaFromFile(options.ServiceMetadataLocation); - } - } - catch (Exception ex) - { - FancyConsole.WriteLine(ConsoleErrorColor, "Error parsing metadata: {0}", ex.Message); - return; - } - - FancyConsole.WriteLine(ConsoleSuccessColor, " found {0} schema definitions: {1}", schemas.Count, (from s in schemas select s.Namespace).ComponentsJoinedByString(", ")); - - var docSet = GetDocSet(options); - - List foundResources = OneDrive.ApiDocumentation.Validation.OData.ODataParser.GenerateResourcesFromSchemas(schemas); - int successCount = 0, errorCount = 0, warningCount = 0; - - foreach (var resource in foundResources) - { - FancyConsole.Write(ConsoleHeaderColor, "Checking resource: {0}...", resource.Metadata.ResourceType); - - FancyConsole.VerboseWriteLine(); - FancyConsole.VerboseWriteLine(resource.JsonExample); - FancyConsole.VerboseWriteLine(); - - // Verify that this resource matches the documentation - ValidationError[] errors; - docSet.ResourceCollection.ValidateJsonExample(resource.Metadata, resource.JsonExample, out errors); - - if (!errors.WereErrors() && !errors.WereWarnings()) - { - FancyConsole.WriteLine(ConsoleSuccessColor, " no errors."); - successCount++; - } - else - { - if (errors.WereWarnings() && !errors.WereErrors()) - warningCount++; - else - errorCount++; - - FancyConsole.WriteLine(); - WriteOutErrors(errors, " "); - } - FancyConsole.WriteLine(); - } - - PrintStatusMessage(successCount, warningCount, errorCount); - - Exit(failure: false); - } - } -} +using Newtonsoft.Json; +using OneDrive.ApiDocumentation.Validation; +using OneDrive.ApiDocumentation.Validation.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OneDrive.ApiDocumentation.ConsoleApp +{ + class Program + { + private const int ExitCodeFailure = 1; + private const int ExitCodeSuccess = 0; + + private const ConsoleColor ConsoleDefaultColor = ConsoleColor.White; + private const ConsoleColor ConsoleHeaderColor = ConsoleColor.Cyan; + private const ConsoleColor ConsoleSubheaderColor = ConsoleColor.DarkCyan; + private const ConsoleColor ConsoleCodeColor = ConsoleColor.Gray; + private const ConsoleColor ConsoleErrorColor = ConsoleColor.Red; + private const ConsoleColor ConsoleWarningColor = ConsoleColor.Yellow; + private const ConsoleColor ConsoleSuccessColor = ConsoleColor.Green; + + static void Main(string[] args) + { + string verbName = null; + BaseOptions verbOptions = null; + + var options = new CommandLineOptions(); + if (!CommandLine.Parser.Default.ParseArguments(args, options, + (verb, subOptions) => + { + // if parsing succeeds the verb name and correct instance + // will be passed to onVerbCommand delegate (string,object) + verbName = verb; + verbOptions = (BaseOptions)subOptions; + })) + { + Exit(failure: true); + } + + var commandOptions = verbOptions as DocSetOptions; + if (null != commandOptions) + { + FancyConsole.WriteVerboseOutput = commandOptions.Verbose; + } + + FancyConsole.LogFileName = verbOptions.LogFile; + +#if DEBUG + System.Diagnostics.Debugger.Launch(); +#endif + + + Nito.AsyncEx.AsyncContext.Run(() => RunInvokedMethodAsync(options, verbName, verbOptions)); + } + + private static async Task RunInvokedMethodAsync(CommandLineOptions origCommandLineOpts, string invokedVerb, BaseOptions options) + { + string[] missingProps; + if (!options.HasRequiredProperties(out missingProps)) + { + if (options is SetCommandOptions) + { + // Just print out the current values of the set parameters + FancyConsole.WriteLine(origCommandLineOpts.GetUsage(invokedVerb)); + FancyConsole.WriteLine(); + WriteSavedValues(SavedSettings.Default); + Exit(failure: true); + } + var error = new ValidationError(ValidationErrorCode.MissingRequiredArguments, null, "Command line is missing required arguments: {0}", missingProps.ComponentsJoinedByString(", ")); + FancyConsole.WriteLine(origCommandLineOpts.GetUsage(invokedVerb)); + WriteOutErrors(new ValidationError[] { error }); + Exit(failure: true); + } + + switch (invokedVerb) + { + case CommandLineOptions.VerbPrint: + PrintDocInformation((PrintOptions)options); + break; + case CommandLineOptions.VerbCheckLinks: + VerifyLinks((DocSetOptions)options); + break; + case CommandLineOptions.VerbDocs: + CheckMethodExamples((ConsistencyCheckOptions)options); + break; + case CommandLineOptions.VerbService: + await CheckMethodsAgainstService((ServiceConsistencyOptions)options); + break; + case CommandLineOptions.VerbSet: + SetDefaultValues((SetCommandOptions)options); + break; + case CommandLineOptions.VerbClean: + await PublishDocumentationAsync((PublishOptions)options); + break; + case CommandLineOptions.VerbMetadata: + await CheckServiceMetadata((CheckMetadataOptions)options); + break; + } + } + + + private static void SetDefaultValues(SetCommandOptions setCommandOptions) + { + var settings = SavedSettings.Default; + if (setCommandOptions.ResetStoredValues) + { + settings.AccessToken = null; + settings.DocumentationPath = null; + settings.ServiceUrl = null; + } + + bool setValues = false; + + if (!string.IsNullOrEmpty(setCommandOptions.AccessToken)) + { + settings.AccessToken = setCommandOptions.AccessToken; + setValues = true; + } + + if (!string.IsNullOrEmpty(setCommandOptions.DocumentationPath)) + { + settings.DocumentationPath = setCommandOptions.DocumentationPath; + setValues = true; + } + + if (!string.IsNullOrEmpty(setCommandOptions.ServiceUrl)) + { + settings.ServiceUrl = setCommandOptions.ServiceUrl; + setValues = true; + } + + settings.Save(); + + if (setCommandOptions.PrintValues || setValues) + { + WriteSavedValues(settings); + } + + } + + private static void WriteSavedValues(SavedSettings settings) + { + FancyConsole.WriteLine(ConsoleHeaderColor, "Stored settings:"); + FancyConsole.WriteLineIndented(" ", "{0}: {1}", "AccessToken", settings.AccessToken); + FancyConsole.WriteLineIndented(" ", "{0}: {1}", "DocumentationPath", settings.DocumentationPath); + FancyConsole.WriteLineIndented(" ", "{0}: {1}", "ServiceUrl", settings.ServiceUrl); + } + + + /// + /// Create a document set based on input options + /// + /// + /// + private static DocSet GetDocSet(DocSetOptions options) + { + FancyConsole.VerboseWriteLine("Opening documentation from {0}", options.PathToDocSet); + DocSet set = new DocSet(options.PathToDocSet); + + FancyConsole.VerboseWriteLine("Scanning documentation files..."); + ValidationError[] loadErrors; + if (!set.ScanDocumentation(out loadErrors) && options.ShowLoadWarnings) + { + WriteOutErrors(loadErrors); + } + + var serviceOptions = options as ServiceConsistencyOptions; + if (null != serviceOptions) + { + FancyConsole.VerboseWriteLine("Reading configuration parameters..."); + set.LoadTestScenarios(serviceOptions.ScenarioFilePath); + if (!set.TestScenarios.Loaded) + { + FancyConsole.WriteLine(ConsoleWarningColor, "Unable to read request parameter configuration file: {0}", serviceOptions.ScenarioFilePath); + } + } + + return set; + } + + private static void PrintDocInformation(PrintOptions options) + { + DocSet docset = GetDocSet(options); + if (options.PrintFiles) + { + PrintFiles(options, docset); + } + if (options.PrintResources) + { + PrintResources(options, docset); + } + if (options.PrintMethods) + { + PrintMethods(options, docset); + } + } + + private static void PrintFiles(DocSetOptions options, DocSet docset) + { + if (null == docset) + docset = GetDocSet(options); + + FancyConsole.WriteLine(); + FancyConsole.WriteLine(ConsoleHeaderColor, "Documentation files"); + + string format = null; + if (options.Verbose) + format = "{1} (resources: {2}, methods: {3})"; + else if (options.ShortForm) + format = "{0}"; + else + format = "{0} (r:{2}, m:{3})"; + + foreach (var file in docset.Files) + { + ConsoleColor color = ConsoleSuccessColor; + if (file.Resources.Length == 0 && file.Requests.Length == 0) + color = ConsoleWarningColor; + + FancyConsole.WriteLineIndented(" ", color, format, file.DisplayName, file.FullPath, file.Resources.Length, file.Requests.Length); + } + } + + private static void VerifyLinks(DocSetOptions options) + { + var docset = GetDocSet(options); + ValidationError[] errors; + docset.ValidateLinks(options.Verbose, out errors); + + if (null != errors && errors.Length > 0) + { + WriteOutErrors(errors); + if (errors.All(x => !x.IsWarning && !x.IsError)) + { + FancyConsole.WriteLine(ConsoleSuccessColor, "No link errors detected."); + Exit(failure: false); + } + } + else + { + FancyConsole.WriteLine(ConsoleSuccessColor, "No link errors detected."); + Exit(failure: false); + } + } + + private static void PrintResources(DocSetOptions options, DocSet docset) + { + if (null == docset) + docset = GetDocSet(options); + + FancyConsole.WriteLine(); + FancyConsole.WriteLine(ConsoleHeaderColor, "Defined resources:"); + FancyConsole.WriteLine(); + + var sortedResources = docset.Resources.OrderBy(x => x.ResourceType); + + foreach (var resource in sortedResources) + { + + + if (!options.ShortForm && options.Verbose) + { + string metadata = JsonConvert.SerializeObject(resource.Metadata); + FancyConsole.Write(" "); + FancyConsole.Write(ConsoleHeaderColor, resource.ResourceType); + FancyConsole.WriteLine(" flags: {1}", resource.ResourceType, metadata); + } + else + { + FancyConsole.WriteLineIndented(" ", resource.ResourceType); + } + + if (!options.ShortForm) + { + FancyConsole.WriteLineIndented(" ", ConsoleCodeColor, resource.JsonExample); + FancyConsole.WriteLine(); + } + } + } + + private static void PrintMethods(DocSetOptions options, DocSet docset) + { + if (null == docset) + docset = GetDocSet(options); + + FancyConsole.WriteLine(); + FancyConsole.WriteLine(ConsoleHeaderColor, "Defined methods:"); + FancyConsole.WriteLine(); + + foreach (var method in docset.Methods) + { + FancyConsole.WriteLine(ConsoleHeaderColor, "Method '{0}' in file '{1}'", method.DisplayName, method.SourceFile.DisplayName); + + if (!options.ShortForm) + { + var requestMetadata = options.Verbose ? JsonConvert.SerializeObject(method.RequestMetadata) : string.Empty; + FancyConsole.WriteLineIndented(" ", ConsoleSubheaderColor, "Request: {0}", requestMetadata); + FancyConsole.WriteLineIndented(" ", ConsoleCodeColor, method.Request); + } + + if (options.Verbose) + { + FancyConsole.WriteLine(); + var responseMetadata = JsonConvert.SerializeObject(method.ExpectedResponseMetadata); + if (options.ShortForm) + FancyConsole.WriteLineIndented(" ", ConsoleHeaderColor, "Expected Response: {0}", method.ExpectedResponse.TopLineOnly()); + else + { + FancyConsole.WriteLineIndented(" ", ConsoleSubheaderColor, "Expected Response: {0}", responseMetadata); + FancyConsole.WriteLineIndented(" ", ConsoleCodeColor, method.ExpectedResponse); + } + FancyConsole.WriteLine(); + } + FancyConsole.WriteLine(); + } + } + + private static MethodDefinition LookUpMethod(DocSet docset, string methodName) + { + var query = from m in docset.Methods + where m.DisplayName.Equals(methodName, StringComparison.OrdinalIgnoreCase) + select m; + + return query.FirstOrDefault(); + } + + private static void CheckMethodExamples(ConsistencyCheckOptions options) + { + var docset = GetDocSet(options); + FancyConsole.WriteLine(); + + MethodDefinition[] methods = FindTestMethods(options, docset); + + bool result = true; + int successCount = 0, errorCount = 0, warningCount = 0; + foreach (var method in methods) + { + FancyConsole.Write(ConsoleHeaderColor, "Checking \"{0}\" in {1}...", method.DisplayName, method.SourceFile.DisplayName); + + if (string.IsNullOrEmpty(method.ExpectedResponse)) + { + FancyConsole.WriteLine(); + FancyConsole.WriteLine(ConsoleErrorColor, " Error: response was null."); + errorCount++; + continue; + } + + var parser = new HttpParser(); + var expectedResponse = parser.ParseHttpResponse(method.ExpectedResponse); + ValidationError[] errors = ValidateHttpResponse(docset, method, expectedResponse); + result &= errors.WereErrors(); + + if (errors.WereErrors()) + { + errorCount++; + } + else if (errors.WereWarnings()) + { + warningCount++; + } + else + { + successCount++; + } + } + + if (options.IgnoreWarnings) + { + successCount += warningCount; + warningCount = 0; + } + + PrintStatusMessage(successCount, warningCount, errorCount); + + Exit(failure: !result); + } + + private static MethodDefinition[] FindTestMethods(ConsistencyCheckOptions options, DocSet docset) + { + MethodDefinition[] methods = null; + if (!string.IsNullOrEmpty(options.MethodName)) + { + var foundMethod = LookUpMethod(docset, options.MethodName); + if (null == foundMethod) + { + FancyConsole.WriteLine(ConsoleErrorColor, "Unable to locate method '{0}' in docset.", options.MethodName); + Exit(failure: true); + } + methods = new MethodDefinition[] { LookUpMethod(docset, options.MethodName) }; + } + else if (!string.IsNullOrEmpty(options.FileName)) + { + var selectedFileQuery = from f in docset.Files where f.DisplayName == options.FileName select f; + var selectedFile = selectedFileQuery.SingleOrDefault(); + if (selectedFile == null) + { + FancyConsole.WriteLine(ConsoleErrorColor, "Unable to locate file '{0}' in docset.", options.FileName); + Exit(failure: true); + } + methods = selectedFile.Requests; + } + else + { + methods = docset.Methods; + } + return methods; + } + + private static void WriteOutErrors(IEnumerable errors, string indent = "") + { + foreach (var error in errors) + { + if (!error.IsWarning && !error.IsError && !FancyConsole.WriteVerboseOutput) + continue; + WriteValidationError(indent, error); + } + } + + private static void WriteValidationError(string indent, ValidationError error) + { + ConsoleColor color; + if (error.IsWarning) + color = ConsoleWarningColor; + else if (error.IsError) + color = ConsoleErrorColor; + else + color = ConsoleDefaultColor; + + FancyConsole.WriteLineIndented(indent, color, error.ErrorText); + } + + private static ValidationError[] ValidateHttpResponse(DocSet docset, MethodDefinition method, HttpResponse response, HttpResponse expectedResponse = null, string indentLevel = "") + { + ValidationError[] errors; + if (!docset.ValidateApiMethod(method, response, expectedResponse, out errors)) + { + FancyConsole.WriteLine(); + WriteOutErrors(errors, indentLevel + " "); + FancyConsole.WriteLine(); + } + else + { + FancyConsole.WriteLine(ConsoleSuccessColor, " no errors"); + } + return errors; + } + + /// + /// Make requests against the service. Uses DocSet.RunParameter information to alter requests. + /// + /// + /// + private static async Task CheckMethodsAgainstService(ServiceConsistencyOptions options) + { + var docset = GetDocSet(options); + FancyConsole.WriteLine(); + + var methods = FindTestMethods(options, docset); + int successCount = 0, warningCount = 0, errorCount = 0; + bool result = true; + foreach (var method in methods) + { + ValidationError[] errors = null; + + FancyConsole.Write(ConsoleHeaderColor, "Calling method \"{0}\"...", method.DisplayName); + if (method.RequestMetadata.Disabled) + { + errors = new ValidationError[] { new ValidationWarning(ValidationErrorCode.MethodDisabled, null, "Method was disabled: {0}", method.DisplayName) }; + FancyConsole.WriteLine(); + WriteOutErrors(errors, " "); + warningCount++; + continue; + } + + AuthenicationCredentials credentials = AuthenicationCredentials.CreateAutoCredentials(options.AccessToken); + var setsOfParameters = docset.TestScenarios.ScenariosForMethod(method); + if (setsOfParameters.Length == 0) + { + // If there are no parameters defined, we still try to call the request as-is. + errors = await TestMethodWithParameters(docset, method, null, options.ServiceRootUrl, credentials); + if (errors.WereErrors()) + { + errorCount++; + } + else if (errors.WereWarnings()) + { + warningCount++; + } + else + { + successCount++; + } + AddPause(options); + } + else + { + // Otherwise, if there are parameter sets, we call each of them and check the result. + foreach (var requestSettings in setsOfParameters.Where(s => s.Enabled)) + { + errors = await TestMethodWithParameters(docset, method, requestSettings, options.ServiceRootUrl, credentials); + if (errors.WereErrors()) + { + errorCount++; + } + else if (errors.WereWarnings()) + { + warningCount++; + } + else + { + successCount++; + } + AddPause(options); + } + } + + FancyConsole.WriteLine(); + } + + if (options.IgnoreWarnings) + { + successCount += warningCount; + warningCount = 0; + } + + PrintStatusMessage(successCount, warningCount, errorCount); + result = (errorCount > 0) || (warningCount > 0); + Exit(!result); + } + + private static void Exit(bool failure) + { +#if DEBUG + if (System.Diagnostics.Debugger.IsAttached) + { + Console.WriteLine(); + Console.Write("Press any key to exit."); + Console.ReadKey(); + } +#endif + + Environment.Exit(failure ? ExitCodeFailure : ExitCodeSuccess); + } + + + private static void PrintStatusMessage(int successCount, int warningCount, int errorCount) + { + FancyConsole.WriteLine(); + FancyConsole.Write("Runs completed. "); + var totalCount = successCount + warningCount + errorCount; + double percentSuccessful = 100 * (successCount / (double)totalCount); + + const string percentCompleteFormat = "{0:0.00}% passed"; + if (percentSuccessful == 100.0) + FancyConsole.Write(ConsoleSuccessColor, percentCompleteFormat, percentSuccessful); + else + FancyConsole.Write(ConsoleWarningColor, percentCompleteFormat, percentSuccessful); + + if (errorCount > 0 || warningCount > 0) + { + FancyConsole.Write(" ("); + if (errorCount > 0) + FancyConsole.Write(ConsoleErrorColor, "{0} errors", errorCount); + if (warningCount > 0 && errorCount > 0) + FancyConsole.Write(", "); + if (warningCount > 0) + FancyConsole.Write(ConsoleWarningColor, "{0} warnings", warningCount); + if (warningCount > 0 || errorCount > 0 && successCount > 0) + FancyConsole.Write(", "); + if (successCount > 0) + FancyConsole.Write(ConsoleSuccessColor, "{0} successful", successCount); + FancyConsole.Write(")"); + } + FancyConsole.WriteLine(); + } + + private static void AddPause(ServiceConsistencyOptions options) + { + if (options.PauseBetweenRequests) + { + FancyConsole.Write("Press any key to continue"); + Console.ReadKey(); + FancyConsole.WriteLine(); + } + } + + private static async Task TestMethodWithParameters(DocSet docset, MethodDefinition method, ScenarioDefinition requestSettings, string rootUrl, AuthenicationCredentials credentials) + { + string indentLevel = ""; + if (requestSettings != null) + { + FancyConsole.WriteLine(); + FancyConsole.Write(ConsoleHeaderColor, " With scenario \"{1}\"...", method.DisplayName, requestSettings.Description); + indentLevel = " "; + } + + FancyConsole.VerboseWriteLine(""); + FancyConsole.VerboseWriteLineIndented(indentLevel, "Request:"); + var requestPreviewResult = await method.PreviewRequestAsync(requestSettings, rootUrl, credentials); + if (requestPreviewResult.IsWarningOrError) + { + WriteOutErrors(requestPreviewResult.Messages, indentLevel + " "); + return requestPreviewResult.Messages; + } + + var requestPreview = requestPreviewResult.Value; + FancyConsole.VerboseWriteLineIndented(indentLevel + " ", requestPreview.FullHttpText()); + + var parser = new HttpParser(); + var expectedResponse = parser.ParseHttpResponse(method.ExpectedResponse); + + var request = requestPreview.PrepareHttpWebRequest(rootUrl); + var actualResponse = await HttpResponse.ResponseFromHttpWebResponseAsync(request); + + FancyConsole.VerboseWriteLineIndented(indentLevel, "Response:"); + FancyConsole.VerboseWriteLineIndented(indentLevel + " ", actualResponse.FullHttpText()); + FancyConsole.VerboseWriteLine(); + + FancyConsole.VerboseWriteLineIndented(indentLevel, "Validation results:"); + return ValidateHttpResponse(docset, method, actualResponse, expectedResponse, indentLevel); + } + + private static async Task PublishDocumentationAsync(PublishOptions options) + { + var outputPath = options.OutputDirectory; + var inputPath = options.PathToDocSet; + + FancyConsole.WriteLine("Publishing documentation to {0}", outputPath); + + DocSet docs = GetDocSet(options); + DocumentPublisher publisher = null; + switch (options.Format) + { + case PublishOptions.SanitizedFormat.Markdown: + publisher = new DocumentPublisher(docs); + break; + case PublishOptions.SanitizedFormat.Html: + publisher = new DocumentPublisherHtml(docs); + break; + case PublishOptions.SanitizedFormat.Swagger2: + publisher = new OneDrive.ApiDocumentation.Validation.Writers.SwaggerWriter(docs); + break; + default: + throw new NotSupportedException("Unsupported format: " + options.Format.ToString()); + } + + publisher.VerboseLogging = options.Verbose; + publisher.SourceFileExtensions = options.TextFileExtensions; + FancyConsole.WriteLineIndented(" ", "File extensions: {0}", publisher.SourceFileExtensions); + + if (!string.IsNullOrEmpty(options.IgnorePaths)) + publisher.SkipPaths = options.IgnorePaths; + FancyConsole.WriteLineIndented(" ", "Ignored Paths: {0}", publisher.SkipPaths); + publisher.PublishAllFiles = options.PublishAllFiles; + FancyConsole.WriteLineIndented(" ", "Include all files: {0}", publisher.PublishAllFiles); + FancyConsole.WriteLine(); + + FancyConsole.WriteLine("Publishing content..."); + publisher.NewMessage += publisher_NewMessage; + await publisher.PublishToFolderAsync(outputPath); + + FancyConsole.WriteLine(ConsoleSuccessColor, "Finished publishing documentation to: {0}", outputPath); + } + + static void publisher_NewMessage(object sender, ValidationError e) + { + if (!FancyConsole.WriteVerboseOutput && !e.IsError && !e.IsWarning) + return; + + WriteValidationError("", e); + } + + private static async Task CheckServiceMetadata(CheckMetadataOptions options) + { + if (string.IsNullOrEmpty(options.ServiceMetadataLocation)) + { + if (!string.IsNullOrEmpty(SavedSettings.Default.ServiceUrl)) + { + options.ServiceMetadataLocation = SavedSettings.Default.ServiceUrl + "$metadata"; + } + else + { + FancyConsole.WriteLine(ConsoleErrorColor, "No service metadata file location specified. Cannot continue."); + Exit(failure: true); + } + } + + FancyConsole.WriteLine(ConsoleHeaderColor, "Loading service metadata from '{0}'...", options.ServiceMetadataLocation); + + Uri metadataUrl; + List schemas = null; + try + { + if (Uri.TryCreate(options.ServiceMetadataLocation, UriKind.Absolute, out metadataUrl)) + { + schemas = await OneDrive.ApiDocumentation.Validation.OData.ODataParser.ReadSchemaFromMetadataUrl(metadataUrl); + } + else + { + schemas = await OneDrive.ApiDocumentation.Validation.OData.ODataParser.ReadSchemaFromFile(options.ServiceMetadataLocation); + } + } + catch (Exception ex) + { + FancyConsole.WriteLine(ConsoleErrorColor, "Error parsing metadata: {0}", ex.Message); + return; + } + + FancyConsole.WriteLine(ConsoleSuccessColor, " found {0} schema definitions: {1}", schemas.Count, (from s in schemas select s.Namespace).ComponentsJoinedByString(", ")); + + var docSet = GetDocSet(options); + + List foundResources = OneDrive.ApiDocumentation.Validation.OData.ODataParser.GenerateResourcesFromSchemas(schemas); + int successCount = 0, errorCount = 0, warningCount = 0; + + foreach (var resource in foundResources) + { + FancyConsole.Write(ConsoleHeaderColor, "Checking resource: {0}...", resource.Metadata.ResourceType); + + FancyConsole.VerboseWriteLine(); + FancyConsole.VerboseWriteLine(resource.JsonExample); + FancyConsole.VerboseWriteLine(); + + // Verify that this resource matches the documentation + ValidationError[] errors; + docSet.ResourceCollection.ValidateJsonExample(resource.Metadata, resource.JsonExample, out errors); + + if (!errors.WereErrors() && !errors.WereWarnings()) + { + FancyConsole.WriteLine(ConsoleSuccessColor, " no errors."); + successCount++; + } + else + { + if (errors.WereWarnings() && !errors.WereErrors()) + warningCount++; + else + errorCount++; + + FancyConsole.WriteLine(); + WriteOutErrors(errors, " "); + } + FancyConsole.WriteLine(); + } + + PrintStatusMessage(successCount, warningCount, errorCount); + + Exit(failure: false); + } + } +} diff --git a/OneDrive.ApiDocumentation.Validation/AuthenicationCredentials.cs b/OneDrive.ApiDocumentation.Validation/AuthenicationCredentials.cs index f738b0fa..1546e361 100644 --- a/OneDrive.ApiDocumentation.Validation/AuthenicationCredentials.cs +++ b/OneDrive.ApiDocumentation.Validation/AuthenicationCredentials.cs @@ -1,69 +1,69 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OneDrive.ApiDocumentation.Validation -{ - /// - /// Authenication Credentials for interacting with the service - /// - public abstract class AuthenicationCredentials - { - public abstract string AuthenicationToken { get; internal set; } - public string FirstPartyApplicationHeaderValue { get; protected set; } - - public static AuthenicationCredentials CreateAutoCredentials(string authenicationToken) - { - if (String.IsNullOrEmpty(authenicationToken)) { return CreateNoCredentials(); } - if (authenicationToken.StartsWith("t=")) - { - return AuthenicationCredentials.CreateWLIDCredentials(authenicationToken); - } - - return AuthenicationCredentials.CreateBearerCredentials(authenicationToken); - } - - public static AuthenicationCredentials CreateBearerCredentials(string authenicationToken) - { - if (String.IsNullOrEmpty(authenicationToken)) { return CreateNoCredentials(); } - return new BearerCredentials { AuthenicationToken = "Bearer " + authenicationToken }; - } - - public static AuthenicationCredentials CreateWLIDCredentials(string authenicationToken) - { - if (String.IsNullOrEmpty(authenicationToken)) { return CreateNoCredentials(); } - return new WLIDCredentials { AuthenicationToken = "WLID1.1 " + authenicationToken }; - } - - public static AuthenicationCredentials CreateNoCredentials() - { - return new NoCredentials(); - } - } - - public class BearerCredentials : AuthenicationCredentials - { - internal BearerCredentials() { } - - public override string AuthenicationToken { get; internal set; } - } - - public class WLIDCredentials : AuthenicationCredentials - { - internal WLIDCredentials() - { - this.FirstPartyApplicationHeaderValue = "SaveToOneDriveWidget"; - } - - public override string AuthenicationToken { get; internal set; } - } - - public class NoCredentials : AuthenicationCredentials - { - internal NoCredentials() { } - - public override string AuthenicationToken { get { return null; } internal set { } } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OneDrive.ApiDocumentation.Validation +{ + /// + /// Authenication Credentials for interacting with the service + /// + public abstract class AuthenicationCredentials + { + public abstract string AuthenicationToken { get; internal set; } + public string FirstPartyApplicationHeaderValue { get; protected set; } + + public static AuthenicationCredentials CreateAutoCredentials(string authenicationToken) + { + if (String.IsNullOrEmpty(authenicationToken)) { return CreateNoCredentials(); } + if (authenicationToken.StartsWith("t=")) + { + return AuthenicationCredentials.CreateWLIDCredentials(authenicationToken); + } + + return AuthenicationCredentials.CreateBearerCredentials(authenicationToken); + } + + public static AuthenicationCredentials CreateBearerCredentials(string authenicationToken) + { + if (String.IsNullOrEmpty(authenicationToken)) { return CreateNoCredentials(); } + return new BearerCredentials { AuthenicationToken = "Bearer " + authenicationToken }; + } + + public static AuthenicationCredentials CreateWLIDCredentials(string authenicationToken) + { + if (String.IsNullOrEmpty(authenicationToken)) { return CreateNoCredentials(); } + return new WLIDCredentials { AuthenicationToken = "WLID1.1 " + authenicationToken }; + } + + public static AuthenicationCredentials CreateNoCredentials() + { + return new NoCredentials(); + } + } + + public class BearerCredentials : AuthenicationCredentials + { + internal BearerCredentials() { } + + public override string AuthenicationToken { get; internal set; } + } + + public class WLIDCredentials : AuthenicationCredentials + { + internal WLIDCredentials() + { + this.FirstPartyApplicationHeaderValue = "SaveToOneDriveWidget"; + } + + public override string AuthenicationToken { get; internal set; } + } + + public class NoCredentials : AuthenicationCredentials + { + internal NoCredentials() { } + + public override string AuthenicationToken { get { return null; } internal set { } } + } +} diff --git a/OneDrive.ApiDocumentation.Validation/CodeBlockAnnotation.cs b/OneDrive.ApiDocumentation.Validation/CodeBlockAnnotation.cs index 8962b630..040f7467 100644 --- a/OneDrive.ApiDocumentation.Validation/CodeBlockAnnotation.cs +++ b/OneDrive.ApiDocumentation.Validation/CodeBlockAnnotation.cs @@ -31,9 +31,9 @@ public class CodeBlockAnnotation /// Speicfy that the result is a collection of the resource type instead of a single instance. /// [JsonProperty("isCollection", DefaultValueHandling = DefaultValueHandling.Ignore)] - public bool IsCollection { get; set; } - - [JsonProperty("isEmpty", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool IsCollection { get; set; } + + [JsonProperty("isEmpty", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool IsEmpty { get; set; } [JsonProperty("truncated", DefaultValueHandling = DefaultValueHandling.Ignore)] diff --git a/OneDrive.ApiDocumentation.Validation/Http/HttpParser.cs b/OneDrive.ApiDocumentation.Validation/Http/HttpParser.cs index 3f012e7b..23f25f6c 100644 --- a/OneDrive.ApiDocumentation.Validation/Http/HttpParser.cs +++ b/OneDrive.ApiDocumentation.Validation/Http/HttpParser.cs @@ -1,130 +1,130 @@ -namespace OneDrive.ApiDocumentation.Validation.Http -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Text; - using System.Threading.Tasks; - using System.IO; - - public class HttpParser - { - - - /// - /// Converts a raw HTTP request into an HttpWebRequest instance - /// - /// - /// - public HttpRequest ParseHttpRequest(string requestString) - { - StringReader reader = new StringReader(requestString); - string line; - ParserMode mode = ParserMode.FirstLine; - - HttpRequest request = new HttpRequest(); - - while( (line = reader.ReadLine()) != null) - { - switch (mode) - { - case ParserMode.FirstLine: - var components = line.Split(' '); - if (components.Length < 2) - throw new ArgumentException("requestString does not contain a proper HTTP request first line."); - - request.Method = components[0]; - request.Url = components[1]; - - mode = ParserMode.Headers; - break; - - case ParserMode.Headers: - if (string.IsNullOrEmpty(line)) - { - mode = ParserMode.Body; - continue; - } - - // Parse each header - int split = line.IndexOf(": "); - if (split < 1) - throw new ArgumentException("requestString contains an invalid header definition"); - - var headerName = line.Substring(0, split); - var headerValue = line.Substring(split + 1); - request.Headers.Add(headerName, headerValue); - - break; - - case ParserMode.Body: - var restOfBody = reader.ReadToEnd() ?? string.Empty; - - // normalize line endings to CRLF, which is required for headers, etc. - restOfBody = restOfBody.Replace("\r\n", "\n").Replace("\n", "\r\n"); - request.Body = line + Environment.NewLine + restOfBody; - break; - } - } - - return request; - } - - public HttpResponse ParseHttpResponse(string responseString) - { - StringReader reader = new StringReader(responseString); - string line; - ParserMode mode = ParserMode.FirstLine; - - HttpResponse response = new HttpResponse() { Headers = new WebHeaderCollection() }; - - while ((line = reader.ReadLine()) != null) - { - switch (mode) - { - case ParserMode.FirstLine: - var components = line.Split(' '); - if (components.Length < 3) throw new ArgumentException("responseString does not contain a proper HTTP request first line."); - - response.HttpVersion = components[0]; - response.StatusCode = int.Parse(components[1]); - response.StatusMessage = components.ComponentsJoinedByString(" ", 2); - - mode = ParserMode.Headers; - break; - - case ParserMode.Headers: - if (string.IsNullOrEmpty(line)) - { - mode = ParserMode.Body; - continue; - } - - // Parse each header - int split = line.IndexOf(": "); - if (split < 1) throw new ArgumentException("requestString contains an invalid header definition"); - - var headerName = line.Substring(0, split); - var headerValue = line.Substring(split + 1); - response.Headers.Add(headerName, headerValue); - - break; - - case ParserMode.Body: - response.Body = line + Environment.NewLine + reader.ReadToEnd(); - break; - } - } - - return response; - } - - private enum ParserMode - { - FirstLine, - Headers, - Body - } - } -} +namespace OneDrive.ApiDocumentation.Validation.Http +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Text; + using System.Threading.Tasks; + using System.IO; + + public class HttpParser + { + + + /// + /// Converts a raw HTTP request into an HttpWebRequest instance + /// + /// + /// + public HttpRequest ParseHttpRequest(string requestString) + { + StringReader reader = new StringReader(requestString); + string line; + ParserMode mode = ParserMode.FirstLine; + + HttpRequest request = new HttpRequest(); + + while( (line = reader.ReadLine()) != null) + { + switch (mode) + { + case ParserMode.FirstLine: + var components = line.Split(' '); + if (components.Length < 2) + throw new ArgumentException("requestString does not contain a proper HTTP request first line."); + + request.Method = components[0]; + request.Url = components[1]; + + mode = ParserMode.Headers; + break; + + case ParserMode.Headers: + if (string.IsNullOrEmpty(line)) + { + mode = ParserMode.Body; + continue; + } + + // Parse each header + int split = line.IndexOf(": "); + if (split < 1) + throw new ArgumentException("requestString contains an invalid header definition"); + + var headerName = line.Substring(0, split); + var headerValue = line.Substring(split + 1); + request.Headers.Add(headerName, headerValue); + + break; + + case ParserMode.Body: + var restOfBody = reader.ReadToEnd() ?? string.Empty; + + // normalize line endings to CRLF, which is required for headers, etc. + restOfBody = restOfBody.Replace("\r\n", "\n").Replace("\n", "\r\n"); + request.Body = line + Environment.NewLine + restOfBody; + break; + } + } + + return request; + } + + public HttpResponse ParseHttpResponse(string responseString) + { + StringReader reader = new StringReader(responseString); + string line; + ParserMode mode = ParserMode.FirstLine; + + HttpResponse response = new HttpResponse() { Headers = new WebHeaderCollection() }; + + while ((line = reader.ReadLine()) != null) + { + switch (mode) + { + case ParserMode.FirstLine: + var components = line.Split(' '); + if (components.Length < 3) throw new ArgumentException("responseString does not contain a proper HTTP request first line."); + + response.HttpVersion = components[0]; + response.StatusCode = int.Parse(components[1]); + response.StatusMessage = components.ComponentsJoinedByString(" ", 2); + + mode = ParserMode.Headers; + break; + + case ParserMode.Headers: + if (string.IsNullOrEmpty(line)) + { + mode = ParserMode.Body; + continue; + } + + // Parse each header + int split = line.IndexOf(": "); + if (split < 1) throw new ArgumentException("requestString contains an invalid header definition"); + + var headerName = line.Substring(0, split); + var headerValue = line.Substring(split + 1); + response.Headers.Add(headerName, headerValue); + + break; + + case ParserMode.Body: + response.Body = line + Environment.NewLine + reader.ReadToEnd(); + break; + } + } + + return response; + } + + private enum ParserMode + { + FirstLine, + Headers, + Body + } + } +} diff --git a/OneDrive.ApiDocumentation.Validation/Http/HttpRequest.cs b/OneDrive.ApiDocumentation.Validation/Http/HttpRequest.cs index 48bfe6e6..28ec095b 100644 --- a/OneDrive.ApiDocumentation.Validation/Http/HttpRequest.cs +++ b/OneDrive.ApiDocumentation.Validation/Http/HttpRequest.cs @@ -1,125 +1,125 @@ -namespace OneDrive.ApiDocumentation.Validation.Http -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net; - using System.Text; - using System.Threading.Tasks; - - public class HttpRequest - { - public HttpRequest() - { - Headers = new WebHeaderCollection(); - } - - public string Method { get; set; } - public string Url { get; set; } - public string Body { get; set; } - - public string Accept - { - get - { - return Headers["Accept"]; - } - set - { - Headers["Accept"] = value; - } - } - - public string Authorization - { - get { return Headers["Authorization"]; } - set { Headers["Authorization"] = value; } - } - - public string ContentType - { - get { return Headers["content-type"]; } - set { Headers["content-type"] = value; } - } - - public bool IsMatchingContentType(string expectedContentType) - { - if (string.IsNullOrEmpty(ContentType)) - return false; - - string[] contentTypeParts = ContentType.Split(new char[] { ';' }); - return contentTypeParts.Length > 0 && contentTypeParts[0].Equals(expectedContentType, StringComparison.OrdinalIgnoreCase); - } - - public WebHeaderCollection Headers { get; private set; } - - public HttpWebRequest PrepareHttpWebRequest(string baseUrl) - { - var effectiveUrl = baseUrl; - if (this.Url.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) - { - effectiveUrl = this.Url; - } - else - { - effectiveUrl += this.Url; - } - - HttpWebRequest request = HttpWebRequest.CreateHttp(effectiveUrl); - request.AllowAutoRedirect = false; - request.Method = Method; - - foreach (var key in Headers.AllKeys) - { - switch (key.ToLower()) - { - case "accept": - request.Accept = Headers[key]; - break; - case "content-type": - request.ContentType = Headers[key]; - break; - case "content-length": - // Don't set these headers - break; - default: - request.Headers.Add(key, Headers[key]); - break; - } - - } - - if (Body != null) - { - using (var stream = request.GetRequestStream()) - { - var writer = new StreamWriter(stream); - writer.Write(Body); - writer.Flush(); - } - } - - return request; - } - - public string FullHttpText() - { - StringBuilder sb = new StringBuilder(); - sb.Append(Method); - sb.Append(" "); - sb.Append(Url); - sb.Append(" "); - sb.AppendLine("HTTP/1.1"); - foreach (var header in Headers.AllKeys) - { - sb.AppendFormat("{0}: {1}", header, Headers[header]); - sb.AppendLine(); - } - sb.AppendLine(); - sb.Append(Body); - - return sb.ToString(); - } - } -} +namespace OneDrive.ApiDocumentation.Validation.Http +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Threading.Tasks; + + public class HttpRequest + { + public HttpRequest() + { + Headers = new WebHeaderCollection(); + } + + public string Method { get; set; } + public string Url { get; set; } + public string Body { get; set; } + + public string Accept + { + get + { + return Headers["Accept"]; + } + set + { + Headers["Accept"] = value; + } + } + + public string Authorization + { + get { return Headers["Authorization"]; } + set { Headers["Authorization"] = value; } + } + + public string ContentType + { + get { return Headers["content-type"]; } + set { Headers["content-type"] = value; } + } + + public bool IsMatchingContentType(string expectedContentType) + { + if (string.IsNullOrEmpty(ContentType)) + return false; + + string[] contentTypeParts = ContentType.Split(new char[] { ';' }); + return contentTypeParts.Length > 0 && contentTypeParts[0].Equals(expectedContentType, StringComparison.OrdinalIgnoreCase); + } + + public WebHeaderCollection Headers { get; private set; } + + public HttpWebRequest PrepareHttpWebRequest(string baseUrl) + { + var effectiveUrl = baseUrl; + if (this.Url.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + effectiveUrl = this.Url; + } + else + { + effectiveUrl += this.Url; + } + + HttpWebRequest request = HttpWebRequest.CreateHttp(effectiveUrl); + request.AllowAutoRedirect = false; + request.Method = Method; + + foreach (var key in Headers.AllKeys) + { + switch (key.ToLower()) + { + case "accept": + request.Accept = Headers[key]; + break; + case "content-type": + request.ContentType = Headers[key]; + break; + case "content-length": + // Don't set these headers + break; + default: + request.Headers.Add(key, Headers[key]); + break; + } + + } + + if (Body != null) + { + using (var stream = request.GetRequestStream()) + { + var writer = new StreamWriter(stream); + writer.Write(Body); + writer.Flush(); + } + } + + return request; + } + + public string FullHttpText() + { + StringBuilder sb = new StringBuilder(); + sb.Append(Method); + sb.Append(" "); + sb.Append(Url); + sb.Append(" "); + sb.AppendLine("HTTP/1.1"); + foreach (var header in Headers.AllKeys) + { + sb.AppendFormat("{0}: {1}", header, Headers[header]); + sb.AppendLine(); + } + sb.AppendLine(); + sb.Append(Body); + + return sb.ToString(); + } + } +} diff --git a/OneDrive.ApiDocumentation.Validation/Json/JsonPath.cs b/OneDrive.ApiDocumentation.Validation/Json/JsonPath.cs index b01d46e6..95b76af7 100644 --- a/OneDrive.ApiDocumentation.Validation/Json/JsonPath.cs +++ b/OneDrive.ApiDocumentation.Validation/Json/JsonPath.cs @@ -1,246 +1,246 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace OneDrive.ApiDocumentation.Validation.Json -{ - public class JsonPath - { - /// - /// Extracts a value out of a JSON object using JSONpath (http://goessner.net/articles/JsonPath/) - /// - /// The from json path. - /// Json. - /// Path. - public static object ValueFromJsonPath(string json, string path) - { - object jsonObject = (JContainer)JsonConvert.DeserializeObject(json); - var currentComponent = DecomposePath(path); - if (currentComponent.IsRoot) - currentComponent = currentComponent.Child; - - while (currentComponent != null) - { - jsonObject = currentComponent.GetObjectForPart(jsonObject); - currentComponent = currentComponent.Child; - } - - return ConvertValueForOutput(jsonObject); - } - - public static object ConvertValueForOutput(object input) - { - JValue value = input as JValue; - if (null != value) - { - switch (value.Type) - { - case JTokenType.Boolean: - case JTokenType.Bytes: - case JTokenType.Date: - case JTokenType.Float: - case JTokenType.Guid: - case JTokenType.Integer: - case JTokenType.Null: - case JTokenType.String: - case JTokenType.TimeSpan: - case JTokenType.Uri: - return value.Value; - default: - break; - } - } - return input; - } - - - /// - /// Sets the value of a property in a dynamic object based on a json path - /// - /// - /// - /// - /// - public static string SetValueForJsonPath(string json, string path, object value) - { - object originalObject = (JContainer)JsonConvert.DeserializeObject(json); - object jsonObject = originalObject; - - var currentComponent = DecomposePath(path); - if (currentComponent.IsRoot) - currentComponent = currentComponent.Child; - - while (currentComponent != null && currentComponent.Child != null) - { - jsonObject = currentComponent.GetObjectForPart(jsonObject, true); - currentComponent = currentComponent.Child; - } - if (null != jsonObject) - { - currentComponent.SetValueForPart(jsonObject, value); - } - - return JsonConvert.SerializeObject(originalObject, Formatting.Indented); - } - - - private static JsonPathPart DecomposePath(string path) - { - var components = path.Split('.'); - if (components.Length < 1 || components[0] != "$") - throw new ArgumentException( - string.Format("Path \"{0}\" doesn't appear to conform to JSONpath syntax.", path), - "path"); - - JsonPathPart root = JsonPathPart.Root; - JsonPathPart currentPart = root; - - for (int i = 1; i < components.Length; i++) - { - var propertyName = components[i]; - int arrayindex = -1; - if (propertyName.EndsWith("]")) - { - int startIndexPosition = propertyName.LastIndexOf("["); - arrayindex = Int32.Parse(propertyName.Substring(startIndexPosition + 1, propertyName.Length - (startIndexPosition + 2))); - propertyName = propertyName.Substring(0, startIndexPosition); - } - - currentPart = new JsonPathPart(currentPart) { PropertyName = propertyName, ArrayIndex = arrayindex }; - } - - return root; - } - - class JsonPathPart - { - public JsonPathPart() - { - ArrayIndex = -1; - } - - public JsonPathPart(JsonPathPart parent) : this() - { - Parent = parent; - if (null != parent) - parent.Child = this; - } - - public bool IsRoot { get { return PropertyName == "$";}} - - public string PropertyName { get; set; } - - public int ArrayIndex { get; set; } - - public JsonPathPart Parent {get;set;} - - public JsonPathPart Child {get;set;} - - public static JsonPathPart Root - { - get { return new JsonPathPart { PropertyName = "$" }; } - } - - - /// - /// Return the value of the property referred to by the current JsonPathPart instance - /// - /// - /// - public object GetObjectForPart(object source, bool createIfMissing = false) - { - JContainer container = source as JContainer; - object foundValue = null; - if (null != container) - { - try - { - foundValue = container[PropertyName]; - } - catch (Exception ex) - { - throw new JsonPathException(string.Format("Couldn't locate property {0}", PropertyName), ex); - } - - if (foundValue == null && (Child != null || ArrayIndex >= 0)) - { - if (!createIfMissing) - { - throw new JsonPathException(string.Format("Property {0} was null or missing. Cannot continue to evaluate path.", PropertyName)); - } - - container[PropertyName] = new JObject(); - foundValue = container[PropertyName]; - } - } - else - { - throw new JsonPathException("Unsupported object type: " + source.ToString()); - } - - if (ArrayIndex >= 0) - { - try - { - foundValue= ((dynamic)foundValue)[ArrayIndex]; - } - catch (Exception ex) - { - throw new JsonPathException("Specified array index was unavailable.", ex); - } - } - - return ConvertValueForOutput(foundValue); - } - - public void SetValueForPart(object source, object value) - { - if (this.Child != null) - { - throw new JsonPathException("Cannot set value for part that isn't a leaf node in the path hierarchy."); - } - - JContainer container = source as JContainer; - if (null != container) - { - try - { - if (null == value) - { - container[PropertyName] = null; - } - else - { - container[PropertyName] = JToken.FromObject(value); - } - } - catch (Exception ex) - { - throw new JsonPathException("Unable to set the value of the property.", ex); - } - } - else - { - throw new JsonPathException("Unsupported object type: " + source.ToString()); - } - } - } - - } - - public class JsonPathException : Exception - { - public JsonPathException(string message) - : base(message) - { - - } - - public JsonPathException(string message, Exception innerException) - : base(message, innerException) - { - - } - } -} - +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace OneDrive.ApiDocumentation.Validation.Json +{ + public class JsonPath + { + /// + /// Extracts a value out of a JSON object using JSONpath (http://goessner.net/articles/JsonPath/) + /// + /// The from json path. + /// Json. + /// Path. + public static object ValueFromJsonPath(string json, string path) + { + object jsonObject = (JContainer)JsonConvert.DeserializeObject(json); + var currentComponent = DecomposePath(path); + if (currentComponent.IsRoot) + currentComponent = currentComponent.Child; + + while (currentComponent != null) + { + jsonObject = currentComponent.GetObjectForPart(jsonObject); + currentComponent = currentComponent.Child; + } + + return ConvertValueForOutput(jsonObject); + } + + public static object ConvertValueForOutput(object input) + { + JValue value = input as JValue; + if (null != value) + { + switch (value.Type) + { + case JTokenType.Boolean: + case JTokenType.Bytes: + case JTokenType.Date: + case JTokenType.Float: + case JTokenType.Guid: + case JTokenType.Integer: + case JTokenType.Null: + case JTokenType.String: + case JTokenType.TimeSpan: + case JTokenType.Uri: + return value.Value; + default: + break; + } + } + return input; + } + + + /// + /// Sets the value of a property in a dynamic object based on a json path + /// + /// + /// + /// + /// + public static string SetValueForJsonPath(string json, string path, object value) + { + object originalObject = (JContainer)JsonConvert.DeserializeObject(json); + object jsonObject = originalObject; + + var currentComponent = DecomposePath(path); + if (currentComponent.IsRoot) + currentComponent = currentComponent.Child; + + while (currentComponent != null && currentComponent.Child != null) + { + jsonObject = currentComponent.GetObjectForPart(jsonObject, true); + currentComponent = currentComponent.Child; + } + if (null != jsonObject) + { + currentComponent.SetValueForPart(jsonObject, value); + } + + return JsonConvert.SerializeObject(originalObject, Formatting.Indented); + } + + + private static JsonPathPart DecomposePath(string path) + { + var components = path.Split('.'); + if (components.Length < 1 || components[0] != "$") + throw new ArgumentException( + string.Format("Path \"{0}\" doesn't appear to conform to JSONpath syntax.", path), + "path"); + + JsonPathPart root = JsonPathPart.Root; + JsonPathPart currentPart = root; + + for (int i = 1; i < components.Length; i++) + { + var propertyName = components[i]; + int arrayindex = -1; + if (propertyName.EndsWith("]")) + { + int startIndexPosition = propertyName.LastIndexOf("["); + arrayindex = Int32.Parse(propertyName.Substring(startIndexPosition + 1, propertyName.Length - (startIndexPosition + 2))); + propertyName = propertyName.Substring(0, startIndexPosition); + } + + currentPart = new JsonPathPart(currentPart) { PropertyName = propertyName, ArrayIndex = arrayindex }; + } + + return root; + } + + class JsonPathPart + { + public JsonPathPart() + { + ArrayIndex = -1; + } + + public JsonPathPart(JsonPathPart parent) : this() + { + Parent = parent; + if (null != parent) + parent.Child = this; + } + + public bool IsRoot { get { return PropertyName == "$";}} + + public string PropertyName { get; set; } + + public int ArrayIndex { get; set; } + + public JsonPathPart Parent {get;set;} + + public JsonPathPart Child {get;set;} + + public static JsonPathPart Root + { + get { return new JsonPathPart { PropertyName = "$" }; } + } + + + /// + /// Return the value of the property referred to by the current JsonPathPart instance + /// + /// + /// + public object GetObjectForPart(object source, bool createIfMissing = false) + { + JContainer container = source as JContainer; + object foundValue = null; + if (null != container) + { + try + { + foundValue = container[PropertyName]; + } + catch (Exception ex) + { + throw new JsonPathException(string.Format("Couldn't locate property {0}", PropertyName), ex); + } + + if (foundValue == null && (Child != null || ArrayIndex >= 0)) + { + if (!createIfMissing) + { + throw new JsonPathException(string.Format("Property {0} was null or missing. Cannot continue to evaluate path.", PropertyName)); + } + + container[PropertyName] = new JObject(); + foundValue = container[PropertyName]; + } + } + else + { + throw new JsonPathException("Unsupported object type: " + source.ToString()); + } + + if (ArrayIndex >= 0) + { + try + { + foundValue= ((dynamic)foundValue)[ArrayIndex]; + } + catch (Exception ex) + { + throw new JsonPathException("Specified array index was unavailable.", ex); + } + } + + return ConvertValueForOutput(foundValue); + } + + public void SetValueForPart(object source, object value) + { + if (this.Child != null) + { + throw new JsonPathException("Cannot set value for part that isn't a leaf node in the path hierarchy."); + } + + JContainer container = source as JContainer; + if (null != container) + { + try + { + if (null == value) + { + container[PropertyName] = null; + } + else + { + container[PropertyName] = JToken.FromObject(value); + } + } + catch (Exception ex) + { + throw new JsonPathException("Unable to set the value of the property.", ex); + } + } + else + { + throw new JsonPathException("Unsupported object type: " + source.ToString()); + } + } + } + + } + + public class JsonPathException : Exception + { + public JsonPathException(string message) + : base(message) + { + + } + + public JsonPathException(string message, Exception innerException) + : base(message, innerException) + { + + } + } +} + diff --git a/OneDrive.ApiDocumentation.Validation/Json/JsonSchema.cs b/OneDrive.ApiDocumentation.Validation/Json/JsonSchema.cs index e1dea057..bf66dc71 100644 --- a/OneDrive.ApiDocumentation.Validation/Json/JsonSchema.cs +++ b/OneDrive.ApiDocumentation.Validation/Json/JsonSchema.cs @@ -94,29 +94,29 @@ public bool ValidateJson(string json, out ValidationError[] errors, Dictionary detectedErrors = null) { JsonProperty propertyInfo = null; @@ -509,19 +509,19 @@ private static JsonProperty ParseProperty(JToken token, JsonSchema containerSche propertyInfo = ParseProperty(tokenProperty.Name, tokenProperty.Value, containerSchema); } else - { - if (detectedErrors != null) - { - detectedErrors.Add( - new ValidationWarning( - ValidationErrorCode.JsonParserException, - token.Path, - "Unhandled token type: " + token.Type)); - } - else - { - Console.WriteLine("Unhandled token type: " + token.Type); - } + { + if (detectedErrors != null) + { + detectedErrors.Add( + new ValidationWarning( + ValidationErrorCode.JsonParserException, + token.Path, + "Unhandled token type: " + token.Type)); + } + else + { + Console.WriteLine("Unhandled token type: " + token.Type); + } } return propertyInfo; } diff --git a/OneDrive.ApiDocumentation.Validation/MethodDefinition.cs b/OneDrive.ApiDocumentation.Validation/MethodDefinition.cs index 629c25b6..23e40d49 100644 --- a/OneDrive.ApiDocumentation.Validation/MethodDefinition.cs +++ b/OneDrive.ApiDocumentation.Validation/MethodDefinition.cs @@ -1,273 +1,273 @@ -using System.Collections.Generic; - -namespace OneDrive.ApiDocumentation.Validation -{ - using System; - using System.Net; - using System.Linq; - using OneDrive.ApiDocumentation.Validation.Http; - using System.Threading.Tasks; - using Newtonsoft.Json; - - /// - /// Definition of a request / response pair for the API - /// - public class MethodDefinition - { - internal const string MimeTypeJson = "application/json"; - internal const string MimeTypeMultipartRelated = "multipart/related"; - internal const string MimeTypePlainText = "text/plain"; - - public MethodDefinition() - { - } - - public static MethodDefinition FromRequest(string request, CodeBlockAnnotation annotation, DocFile source) - { - var method = new MethodDefinition { Request = request, RequestMetadata = annotation }; - method.DisplayName = annotation.MethodName; - method.SourceFile = source; - return method; - } - - - /// - /// Friendly name of this request/response pair - /// - public string DisplayName { get; set; } - - /// - /// The raw request data from the documentation (fenced code block with annotation) - /// - public string Request {get; private set;} - - /// - /// Properties about the Request - /// - public CodeBlockAnnotation RequestMetadata { get; private set; } - - /// - /// The raw response data from the documentation (fenced code block with annotation) - /// - public string ExpectedResponse { get; private set; } - - /// - /// Properties about the Response - /// - public CodeBlockAnnotation ExpectedResponseMetadata { get; set; } - - /// - /// The documentation file that was the source of this method - /// - /// The source file. - public DocFile SourceFile {get; private set;} - - public void AddExpectedResponse(string rawResponse, CodeBlockAnnotation annotation) - { - ExpectedResponse = rawResponse; - ExpectedResponseMetadata = annotation; - } - - public string ActualResponse { get; set; } - - /// - /// Converts the raw HTTP request in Request into a callable HttpWebRequest - /// - /// - /// - /// - public async Task> BuildRequestAsync(string baseUrl, AuthenicationCredentials credentials, ScenarioDefinition scenario = null) - { - var previewResult = await PreviewRequestAsync(scenario, baseUrl, credentials); - if (previewResult.IsWarningOrError) - { - return new ValidationResult(null, previewResult.Messages); - } - - var httpRequest = previewResult.Value; - HttpWebRequest request = httpRequest.PrepareHttpWebRequest(baseUrl); - return new ValidationResult(request); - } - - public async Task> PreviewRequestAsync(ScenarioDefinition scenario, string baseUrl, AuthenicationCredentials credentials) - { - var parser = new HttpParser(); - var request = parser.ParseHttpRequest(Request); - AddAccessTokenToRequest(credentials, request); - - if (null != scenario) - { - var storedValuesForScenario = new Dictionary(); - if (null != scenario.TestSetupRequests) - { - foreach (var setupRequest in scenario.TestSetupRequests) - { - var result = await setupRequest.MakeSetupRequestAsync(baseUrl, credentials, storedValuesForScenario); - if (result.IsWarningOrError) - { - return new ValidationResult(null, result.Messages); - } - } - } - - try - { - var placeholderValues = scenario.RequestParameters.ToPlaceholderValuesArray(storedValuesForScenario); - request.RewriteRequestWithParameters(placeholderValues); - } - catch (Exception ex) - { - // Error when applying parameters to the request - return new ValidationResult(null, new ValidationError(ValidationErrorCode.RewriteRequestFailure, "PreviewRequestAsync", ex.Message)); - } - } - - if (string.IsNullOrEmpty(request.Accept)) - { - request.Accept = MimeTypeJson; - } - - return new ValidationResult(request); - } - - internal static void AddAccessTokenToRequest(AuthenicationCredentials credentials, HttpRequest request) - { - if (!(credentials is NoCredentials) && string.IsNullOrEmpty(request.Authorization)) - { - request.Authorization = credentials.AuthenicationToken; - } - - if (!string.IsNullOrEmpty(credentials.FirstPartyApplicationHeaderValue) && - request.Headers["Application"] == null) - { - request.Headers.Add("Application", credentials.FirstPartyApplicationHeaderValue); - } - } - - internal static string RewriteUrlWithParameters(string url, IEnumerable parameters) - { - foreach (var parameter in parameters) - { - if (parameter.PlaceholderKey == "!url") - { - url = parameter.Value; - } - else if (parameter.PlaceholderKey.StartsWith("{") && parameter.PlaceholderKey.EndsWith("}")) - { - url = url.Replace(parameter.PlaceholderKey, parameter.Value); - } - else - { - string placeholder = string.Concat("{", parameter.PlaceholderKey, "}"); - url = url.Replace(placeholder, parameter.Value); - } - } - - return url; - } - - internal static string RewriteJsonBodyWithParameters(string jsonSource, IEnumerable parameters) - { - if (string.IsNullOrEmpty(jsonSource)) return jsonSource; - - var jsonParameters = (from p in parameters - where p.Location == PlaceholderLocation.Json - select p); - - - foreach (var parameter in jsonParameters) - { - jsonSource = Json.JsonPath.SetValueForJsonPath(jsonSource, parameter.PlaceholderKey, parameter.Value); - } - - return jsonSource; - } - - internal static void RewriteHeadersWithParameters(HttpRequest request, IEnumerable headerParameters) - { - foreach (var param in headerParameters) - { - string headerName = param.PlaceholderKey; - if (param.PlaceholderKey.EndsWith(":")) - headerName = param.PlaceholderKey.Substring(0, param.PlaceholderKey.Length - 1); - - request.Headers[headerName] = param.Value; - } - } - - public async Task> ApiResponseForMethod(string baseUrl, AuthenicationCredentials credentials, ScenarioDefinition scenario = null) - { - var buildResult = await BuildRequestAsync(baseUrl, credentials, scenario); - if (buildResult.IsWarningOrError) - { - return new ValidationResult(null, buildResult.Messages); - } - - var response = await HttpResponse.ResponseFromHttpWebResponseAsync(buildResult.Value); - return new ValidationResult(response); - } - - - class DynamicBinder : System.Dynamic.SetMemberBinder - { - public DynamicBinder(string propertyName) : base(propertyName, true) - { - } - - public override System.Dynamic.DynamicMetaObject FallbackSetMember(System.Dynamic.DynamicMetaObject target, System.Dynamic.DynamicMetaObject value, System.Dynamic.DynamicMetaObject errorSuggestion) - { - throw new NotImplementedException(); - } - } - - /// - /// Check to ensure the http request is valid - /// - /// - internal void VerifyHttpRequest(List detectedErrors) - { - HttpParser parser = new HttpParser(); - HttpRequest request = null; - try - { - request = parser.ParseHttpRequest(Request); - } - catch (Exception ex) - { - detectedErrors.Add(new ValidationError(ValidationErrorCode.HttpParserError, null, "Exception while parsing HTTP request: {0}", ex.Message)); - return; - } - - if (null != request.ContentType) - { - if (request.IsMatchingContentType(MimeTypeJson)) - { - // Verify that the request is valid JSON - try - { - JsonConvert.DeserializeObject(request.Body); - } - catch (Exception ex) - { - detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonParserException, null, "Invalid JSON format: {0}", ex.Message)); - } - } - else if (request.IsMatchingContentType(MimeTypeMultipartRelated)) - { - // TODO: Parse the multipart/form-data body to ensure it's properly formatted - } - else if (request.IsMatchingContentType(MimeTypePlainText)) - { - // Ignore this, because it isn't something we can verify - } - else - { - detectedErrors.Add(new ValidationWarning(ValidationErrorCode.UnsupportedContentType, null, "Unvalidated request content type: {0}", request.ContentType)); - } - } - } - - - } -} - +using System.Collections.Generic; + +namespace OneDrive.ApiDocumentation.Validation +{ + using System; + using System.Net; + using System.Linq; + using OneDrive.ApiDocumentation.Validation.Http; + using System.Threading.Tasks; + using Newtonsoft.Json; + + /// + /// Definition of a request / response pair for the API + /// + public class MethodDefinition + { + internal const string MimeTypeJson = "application/json"; + internal const string MimeTypeMultipartRelated = "multipart/related"; + internal const string MimeTypePlainText = "text/plain"; + + public MethodDefinition() + { + } + + public static MethodDefinition FromRequest(string request, CodeBlockAnnotation annotation, DocFile source) + { + var method = new MethodDefinition { Request = request, RequestMetadata = annotation }; + method.DisplayName = annotation.MethodName; + method.SourceFile = source; + return method; + } + + + /// + /// Friendly name of this request/response pair + /// + public string DisplayName { get; set; } + + /// + /// The raw request data from the documentation (fenced code block with annotation) + /// + public string Request {get; private set;} + + /// + /// Properties about the Request + /// + public CodeBlockAnnotation RequestMetadata { get; private set; } + + /// + /// The raw response data from the documentation (fenced code block with annotation) + /// + public string ExpectedResponse { get; private set; } + + /// + /// Properties about the Response + /// + public CodeBlockAnnotation ExpectedResponseMetadata { get; set; } + + /// + /// The documentation file that was the source of this method + /// + /// The source file. + public DocFile SourceFile {get; private set;} + + public void AddExpectedResponse(string rawResponse, CodeBlockAnnotation annotation) + { + ExpectedResponse = rawResponse; + ExpectedResponseMetadata = annotation; + } + + public string ActualResponse { get; set; } + + /// + /// Converts the raw HTTP request in Request into a callable HttpWebRequest + /// + /// + /// + /// + public async Task> BuildRequestAsync(string baseUrl, AuthenicationCredentials credentials, ScenarioDefinition scenario = null) + { + var previewResult = await PreviewRequestAsync(scenario, baseUrl, credentials); + if (previewResult.IsWarningOrError) + { + return new ValidationResult(null, previewResult.Messages); + } + + var httpRequest = previewResult.Value; + HttpWebRequest request = httpRequest.PrepareHttpWebRequest(baseUrl); + return new ValidationResult(request); + } + + public async Task> PreviewRequestAsync(ScenarioDefinition scenario, string baseUrl, AuthenicationCredentials credentials) + { + var parser = new HttpParser(); + var request = parser.ParseHttpRequest(Request); + AddAccessTokenToRequest(credentials, request); + + if (null != scenario) + { + var storedValuesForScenario = new Dictionary(); + if (null != scenario.TestSetupRequests) + { + foreach (var setupRequest in scenario.TestSetupRequests) + { + var result = await setupRequest.MakeSetupRequestAsync(baseUrl, credentials, storedValuesForScenario); + if (result.IsWarningOrError) + { + return new ValidationResult(null, result.Messages); + } + } + } + + try + { + var placeholderValues = scenario.RequestParameters.ToPlaceholderValuesArray(storedValuesForScenario); + request.RewriteRequestWithParameters(placeholderValues); + } + catch (Exception ex) + { + // Error when applying parameters to the request + return new ValidationResult(null, new ValidationError(ValidationErrorCode.RewriteRequestFailure, "PreviewRequestAsync", ex.Message)); + } + } + + if (string.IsNullOrEmpty(request.Accept)) + { + request.Accept = MimeTypeJson; + } + + return new ValidationResult(request); + } + + internal static void AddAccessTokenToRequest(AuthenicationCredentials credentials, HttpRequest request) + { + if (!(credentials is NoCredentials) && string.IsNullOrEmpty(request.Authorization)) + { + request.Authorization = credentials.AuthenicationToken; + } + + if (!string.IsNullOrEmpty(credentials.FirstPartyApplicationHeaderValue) && + request.Headers["Application"] == null) + { + request.Headers.Add("Application", credentials.FirstPartyApplicationHeaderValue); + } + } + + internal static string RewriteUrlWithParameters(string url, IEnumerable parameters) + { + foreach (var parameter in parameters) + { + if (parameter.PlaceholderKey == "!url") + { + url = parameter.Value; + } + else if (parameter.PlaceholderKey.StartsWith("{") && parameter.PlaceholderKey.EndsWith("}")) + { + url = url.Replace(parameter.PlaceholderKey, parameter.Value); + } + else + { + string placeholder = string.Concat("{", parameter.PlaceholderKey, "}"); + url = url.Replace(placeholder, parameter.Value); + } + } + + return url; + } + + internal static string RewriteJsonBodyWithParameters(string jsonSource, IEnumerable parameters) + { + if (string.IsNullOrEmpty(jsonSource)) return jsonSource; + + var jsonParameters = (from p in parameters + where p.Location == PlaceholderLocation.Json + select p); + + + foreach (var parameter in jsonParameters) + { + jsonSource = Json.JsonPath.SetValueForJsonPath(jsonSource, parameter.PlaceholderKey, parameter.Value); + } + + return jsonSource; + } + + internal static void RewriteHeadersWithParameters(HttpRequest request, IEnumerable headerParameters) + { + foreach (var param in headerParameters) + { + string headerName = param.PlaceholderKey; + if (param.PlaceholderKey.EndsWith(":")) + headerName = param.PlaceholderKey.Substring(0, param.PlaceholderKey.Length - 1); + + request.Headers[headerName] = param.Value; + } + } + + public async Task> ApiResponseForMethod(string baseUrl, AuthenicationCredentials credentials, ScenarioDefinition scenario = null) + { + var buildResult = await BuildRequestAsync(baseUrl, credentials, scenario); + if (buildResult.IsWarningOrError) + { + return new ValidationResult(null, buildResult.Messages); + } + + var response = await HttpResponse.ResponseFromHttpWebResponseAsync(buildResult.Value); + return new ValidationResult(response); + } + + + class DynamicBinder : System.Dynamic.SetMemberBinder + { + public DynamicBinder(string propertyName) : base(propertyName, true) + { + } + + public override System.Dynamic.DynamicMetaObject FallbackSetMember(System.Dynamic.DynamicMetaObject target, System.Dynamic.DynamicMetaObject value, System.Dynamic.DynamicMetaObject errorSuggestion) + { + throw new NotImplementedException(); + } + } + + /// + /// Check to ensure the http request is valid + /// + /// + internal void VerifyHttpRequest(List detectedErrors) + { + HttpParser parser = new HttpParser(); + HttpRequest request = null; + try + { + request = parser.ParseHttpRequest(Request); + } + catch (Exception ex) + { + detectedErrors.Add(new ValidationError(ValidationErrorCode.HttpParserError, null, "Exception while parsing HTTP request: {0}", ex.Message)); + return; + } + + if (null != request.ContentType) + { + if (request.IsMatchingContentType(MimeTypeJson)) + { + // Verify that the request is valid JSON + try + { + JsonConvert.DeserializeObject(request.Body); + } + catch (Exception ex) + { + detectedErrors.Add(new ValidationError(ValidationErrorCode.JsonParserException, null, "Invalid JSON format: {0}", ex.Message)); + } + } + else if (request.IsMatchingContentType(MimeTypeMultipartRelated)) + { + // TODO: Parse the multipart/form-data body to ensure it's properly formatted + } + else if (request.IsMatchingContentType(MimeTypePlainText)) + { + // Ignore this, because it isn't something we can verify + } + else + { + detectedErrors.Add(new ValidationWarning(ValidationErrorCode.UnsupportedContentType, null, "Unvalidated request content type: {0}", request.ContentType)); + } + } + } + + + } +} + diff --git a/OneDrive.ApiDocumentation.Validation/Params/BasicRequestDefinition.cs b/OneDrive.ApiDocumentation.Validation/Params/BasicRequestDefinition.cs index 67c0edaa..f28d2e56 100644 --- a/OneDrive.ApiDocumentation.Validation/Params/BasicRequestDefinition.cs +++ b/OneDrive.ApiDocumentation.Validation/Params/BasicRequestDefinition.cs @@ -59,9 +59,9 @@ public virtual ValidationError[] CheckForErrors() return errors.ToArray(); } - public static PlaceholderLocation LocationForKey(string key) - { - if (null == key) + public static PlaceholderLocation LocationForKey(string key) + { + if (null == key) return PlaceholderLocation.Invalid; if (key.StartsWith("{") && key.EndsWith("}") && key.Length > 2) diff --git a/OneDrive.ApiDocumentation.Validation/Params/RequestDefinitionExtensions.cs b/OneDrive.ApiDocumentation.Validation/Params/RequestDefinitionExtensions.cs index e694e9dd..0fdd4667 100644 --- a/OneDrive.ApiDocumentation.Validation/Params/RequestDefinitionExtensions.cs +++ b/OneDrive.ApiDocumentation.Validation/Params/RequestDefinitionExtensions.cs @@ -77,7 +77,7 @@ public static void RewriteRequestWithParameters(this Http.HttpRequest request, I // Json var jsonParams = from pv in placeholderValues where pv.Location == PlaceholderLocation.Json select pv; if (jsonParams.Count() > 0 && request.IsMatchingContentType("application/json")) - { + { request.Body = MethodDefinition.RewriteJsonBodyWithParameters(request.Body, jsonParams); } } diff --git a/OneDrive.ApiDocumentation.Validation/Params/TestSetupRequestDefinition.cs b/OneDrive.ApiDocumentation.Validation/Params/TestSetupRequestDefinition.cs index f761ba85..8979a9af 100644 --- a/OneDrive.ApiDocumentation.Validation/Params/TestSetupRequestDefinition.cs +++ b/OneDrive.ApiDocumentation.Validation/Params/TestSetupRequestDefinition.cs @@ -82,8 +82,8 @@ public async Task> MakeSetupRequestAsync(string baseUrl, // Check to see if this request is "successful" or not if ( (AllowedStatusCodes == null && response.WasSuccessful) || (AllowedStatusCodes != null && AllowedStatusCodes.Contains(response.StatusCode))) - { - + { + string expectedContentType = (null != OutputValues) ? ExpectedResponseContentType(OutputValues.Values) : null; // Check for content type mismatch @@ -92,16 +92,16 @@ public async Task> MakeSetupRequestAsync(string baseUrl, return new ValidationResult(false, new ValidationError(ValidationErrorCode.UnsupportedContentType, SourceName, "No Content-Type found for a non-204 response")); } - // Load requested values into stored values - if (null != OutputValues) - { - foreach (var outputKey in OutputValues.Keys) - { - var source = OutputValues[outputKey]; - storedValues[outputKey] = response.ValueForKeyedIdentifier(source); - } - } - + // Load requested values into stored values + if (null != OutputValues) + { + foreach (var outputKey in OutputValues.Keys) + { + var source = OutputValues[outputKey]; + storedValues[outputKey] = response.ValueForKeyedIdentifier(source); + } + } + return new ValidationResult(!errors.Any(x => x.IsError), errors); } else