diff --git a/Documentation/spec/docfx_flavored_markdown.md b/Documentation/spec/docfx_flavored_markdown.md index 784765c67bf..e76ea7f521b 100644 --- a/Documentation/spec/docfx_flavored_markdown.md +++ b/Documentation/spec/docfx_flavored_markdown.md @@ -110,8 +110,12 @@ Allows you to insert code with code language specified. The content of specified * __``__ can be made up of any number of character and '-'. However, the recommended value should follow [Highlight.js language names and aliases](http://highlightjs.readthedocs.org/en/latest/css-classes-reference.html#language-names-and-aliases). * __``__ is the relative path in file system which indicates the code snippet file that you want to expand. * __``__ and __``__ are used together to retrieve part of the code snippet file in the line range or tag name way. We have 2 query string options to represent these two ways: - * __`#`__: _`#L{startlinenumber}-L{endlinenumber}`_ (line range) or _`#L{tagname}`_ (tag name) - * __`?`__: _`?start={startlinenumber}&end={endlinenumber}`_ (line range) or _`?{name}={tagname}`_ (tag name) + +| | query string using `#` | query string using `?` +|--------------------------|----------------------------------------|------------- +| 1. line range | `#L{startlinenumber}-L{endlinenumber}` | `?start={startlinenumber}&end={endlinenumber}` +| 2. tagname | `#{tagname}` | `?name={tagname}` +| 3. multiple region range | _Unsupported_ | `?range={rangequerystring}` * __``__ can be omitted. #### Code Snippet Sample @@ -123,6 +127,7 @@ Allows you to insert code with code language specified. The content of specified [!code[Main](index.xml?start=5&end=9)] [!code-javascript[Main](../jquery.js?name=testsnippet)] +[!code[Main](index.xml?range=2,5-7,9-) "This includes the lines 2, 5, 6, 7 and lines 9 to the last line"] ``` #### Tag Name Representation in Code Snippet Source File diff --git a/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/CodeSnippet.cs b/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/CodeSnippet.cs index d3eab29b9b3..1c8d0f8909f 100644 --- a/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/CodeSnippet.cs +++ b/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/CodeSnippet.cs @@ -3,16 +3,6 @@ namespace Microsoft.DocAsCode.Dfm { - using System; - using System.Collections.Generic; - - public class CodeSnippet - { - public string Name { get; set; } - - public List<Tuple<int, int>> LinePair { get; set; } - } - public class CodeSnippetTag { public CodeSnippetTag(string name, int line, CodeSnippetTagType type) diff --git a/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/CodeSnippetRegexExtractor.cs b/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/CodeSnippetRegexExtractor.cs index 79019e82637..bcbe65c65ac 100644 --- a/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/CodeSnippetRegexExtractor.cs +++ b/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/CodeSnippetRegexExtractor.cs @@ -48,7 +48,6 @@ public abstract class CodeSnippetRegexExtractor : ICodeSnippetExtractor tagResolveResult.StartLine = startLine + 1; tagResolveResult.EndLine = endLine - 1; tagResolveResult.ExcludesLines = excludedLines; - tagResolveResult.IndentLength = DfmCodeExtractorHelper.GetIndentLength(lines[startLine - 1]); } else { diff --git a/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/DfmTagNameResolveResult.cs b/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/DfmTagNameResolveResult.cs index ef55a5e0006..87bc2ae509c 100644 --- a/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/DfmTagNameResolveResult.cs +++ b/src/Microsoft.DocAsCode.Dfm/CodeSnippetExtractor/DfmTagNameResolveResult.cs @@ -11,8 +11,6 @@ public class DfmTagNameResolveResult public int EndLine { get; set; } - public int IndentLength { get; set; } - public HashSet<int> ExcludesLines { get; set; } public bool IsSuccessful { get; set; } diff --git a/src/Microsoft.DocAsCode.Dfm/DfmCodeExtractor.cs b/src/Microsoft.DocAsCode.Dfm/DfmCodeExtractor.cs index bf9d58f0431..5e1df642260 100644 --- a/src/Microsoft.DocAsCode.Dfm/DfmCodeExtractor.cs +++ b/src/Microsoft.DocAsCode.Dfm/DfmCodeExtractor.cs @@ -4,7 +4,6 @@ namespace Microsoft.DocAsCode.Dfm { using System; - using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -14,190 +13,8 @@ namespace Microsoft.DocAsCode.Dfm internal class DfmCodeExtractor { - // C# code snippet comment block: // <[/]snippetname> - private static readonly Regex CSharpCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex CSharpCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - // C# code snippet region block: start -> #region snippetname, end -> #endregion - private static readonly Regex CSharpCodeSnippetRegionStartLineRegex = new Regex(@"^\s*#\s*region\s+(?<name>.+?)\s*$", RegexOptions.Compiled); - private static readonly Regex CSharpCodeSnippetRegionEndLineRegex = new Regex(@"^\s*#\s*endregion\s*$", RegexOptions.Compiled); - - // VB code snippet comment block: ' <[/]snippetname> - private static readonly Regex VBCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\'\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex VBCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\'\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - // VB code snippet Region block: start -> # Region "snippetname", end -> # End Region - private static readonly Regex VBCodeSnippetRegionRegionStartLineRegex = new Regex(@"^\s*#\s*Region\s*(?<name>.+?)\s*$", RegexOptions.Compiled); - private static readonly Regex VBCodeSnippetRegionRegionEndLineRegex = new Regex(@"^\s*#\s*End\s+Region\s*$", RegexOptions.Compiled); - - // C++ code snippet block: // <[/]snippetname> - private static readonly Regex CPlusPlusCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex CPlusPlusCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - // F# code snippet block: // <[/]snippetname> - private static readonly Regex FSharpCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex FSharpCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - // XML code snippet block: <!-- <[/]snippetname> --> - private static readonly Regex XmlCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex XmlCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - // XAML code snippet block: <!-- <[/]snippetname> --> - private static readonly Regex XamlCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex XamlCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - // HTML code snippet block: <!-- <[/]snippetname> --> - private static readonly Regex HtmlCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex HtmlCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - // Sql code snippet block: -- <[/]snippetname> - private static readonly Regex SqlCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\-{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SqlCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\-{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - // Javascript code snippet block: <!-- <[/]snippetname> --> - private static readonly Regex JavaScriptSnippetCommentStartLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex JavaScriptSnippetCommentEndLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly string RemoveIndentSpacesRegexString = @"^[ \t]{{1,{0}}}"; - // Language names and aliases fllow http://highlightjs.readthedocs.org/en/latest/css-classes-reference.html#language-names-and-aliases - // Language file extensions follow https://github.com/github/linguist/blob/master/lib/linguist/languages.yml - // Currently only supports parts of the language names, aliases and extensions - // Later we can move the repository's supported/custom language names, aliases, extensions and corresponding comments regexes to docfx build configuration - private static readonly IReadOnlyDictionary<string, List<ICodeSnippetExtractor>> CodeLanguageExtractors = - new Dictionary<string, List<ICodeSnippetExtractor>>(StringComparer.OrdinalIgnoreCase) - { - [".cs"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(CSharpCodeSnippetCommentStartLineRegex, CSharpCodeSnippetCommentEndLineRegex), - new RecursiveNameCodeSnippetExtractor(CSharpCodeSnippetRegionStartLineRegex, CSharpCodeSnippetRegionEndLineRegex) - }, - ["cs"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(CSharpCodeSnippetCommentStartLineRegex, CSharpCodeSnippetCommentEndLineRegex), - new RecursiveNameCodeSnippetExtractor(CSharpCodeSnippetRegionStartLineRegex, CSharpCodeSnippetRegionEndLineRegex) - }, - ["csharp"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(CSharpCodeSnippetCommentStartLineRegex, CSharpCodeSnippetCommentEndLineRegex), - new RecursiveNameCodeSnippetExtractor(CSharpCodeSnippetRegionStartLineRegex, CSharpCodeSnippetRegionEndLineRegex) - }, - [".vb"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(VBCodeSnippetCommentStartLineRegex, VBCodeSnippetCommentEndLineRegex), - new RecursiveNameCodeSnippetExtractor(VBCodeSnippetRegionRegionStartLineRegex, VBCodeSnippetRegionRegionEndLineRegex) - }, - ["vb"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(VBCodeSnippetCommentStartLineRegex, VBCodeSnippetCommentEndLineRegex), - new RecursiveNameCodeSnippetExtractor(VBCodeSnippetRegionRegionStartLineRegex, VBCodeSnippetRegionRegionEndLineRegex) - }, - ["vbnet"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(VBCodeSnippetCommentStartLineRegex, VBCodeSnippetCommentEndLineRegex), - new RecursiveNameCodeSnippetExtractor(VBCodeSnippetRegionRegionStartLineRegex, VBCodeSnippetRegionRegionEndLineRegex) - }, - [".cpp"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) - }, - [".h"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) - }, - [".hpp"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) - }, - [".c"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) - }, - [".cc"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) - }, - ["cpp"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) - }, - ["c++"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) - }, - ["fs"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(FSharpCodeSnippetCommentStartLineRegex, FSharpCodeSnippetCommentEndLineRegex) - }, - ["fsharp"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(FSharpCodeSnippetCommentStartLineRegex, FSharpCodeSnippetCommentEndLineRegex) - }, - [".fs"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(FSharpCodeSnippetCommentStartLineRegex, FSharpCodeSnippetCommentEndLineRegex) - }, - [".fsi"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(FSharpCodeSnippetCommentStartLineRegex, FSharpCodeSnippetCommentEndLineRegex) - }, - [".fsx"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(FSharpCodeSnippetCommentStartLineRegex, FSharpCodeSnippetCommentEndLineRegex) - }, - [".xml"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(XmlCodeSnippetCommentStartLineRegex, XmlCodeSnippetCommentEndLineRegex) - }, - [".csdl"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(XmlCodeSnippetCommentStartLineRegex, XmlCodeSnippetCommentEndLineRegex) - }, - [".edmx"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(XmlCodeSnippetCommentStartLineRegex, XmlCodeSnippetCommentEndLineRegex) - }, - ["xml"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(XmlCodeSnippetCommentStartLineRegex, XmlCodeSnippetCommentEndLineRegex) - }, - [".html"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(HtmlCodeSnippetCommentStartLineRegex, HtmlCodeSnippetCommentEndLineRegex) - }, - ["html"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(HtmlCodeSnippetCommentStartLineRegex, HtmlCodeSnippetCommentEndLineRegex) - }, - [".xaml"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(XamlCodeSnippetCommentStartLineRegex, XamlCodeSnippetCommentEndLineRegex) - }, - [".sql"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(SqlCodeSnippetCommentStartLineRegex, SqlCodeSnippetCommentEndLineRegex) - }, - ["sql"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(SqlCodeSnippetCommentStartLineRegex, SqlCodeSnippetCommentEndLineRegex) - }, - [".js"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(JavaScriptSnippetCommentStartLineRegex, JavaScriptSnippetCommentEndLineRegex) - }, - ["js"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(JavaScriptSnippetCommentStartLineRegex, JavaScriptSnippetCommentEndLineRegex) - }, - ["javascript"] = new List<ICodeSnippetExtractor> - { - new FlatNameCodeSnippetExtractor(JavaScriptSnippetCommentStartLineRegex, JavaScriptSnippetCommentEndLineRegex) - } - }; - - private readonly ConcurrentDictionary<string, Lazy<ConcurrentDictionary<string, DfmTagNameResolveResult>>> _dfmTagNameLineRangeCache - = new ConcurrentDictionary<string, Lazy<ConcurrentDictionary<string, DfmTagNameResolveResult>>>(StringComparer.OrdinalIgnoreCase); - public DfmExtractCodeResult ExtractFencesCode(DfmFencesBlockToken token, string fencesPath) { if (token == null) @@ -211,116 +28,32 @@ public DfmExtractCodeResult ExtractFencesCode(DfmFencesBlockToken token, string } var fencesCode = File.ReadAllLines(fencesPath); - - // NOTE: Parsing language and removing comment lines only do for tag name representation - if (token.PathQueryOption?.TagName != null) + if (token.PathQueryOption == null) { - var lang = GetCodeLanguageOrExtension(token); - List<ICodeSnippetExtractor> extractors; - if (!CodeLanguageExtractors.TryGetValue(lang, out extractors)) - { - string errorMessage = $"{lang} is not supported languaging name, alias or extension for parsing code snippet with tag name, you can use line numbers instead"; - Logger.LogError(errorMessage); - return new DfmExtractCodeResult { IsSuccessful = false, ErrorMessage = errorMessage, FencesCodeLines = fencesCode }; - } - - var resolveResult = ResolveTagNamesFromPath(fencesPath, fencesCode, token.PathQueryOption.TagName, extractors); - if (!resolveResult.IsSuccessful) - { - Logger.LogError(resolveResult.ErrorMessage); - return new DfmExtractCodeResult { IsSuccessful = false, ErrorMessage = resolveResult.ErrorMessage, FencesCodeLines = fencesCode }; - } - - return GetFencesCodeCore(fencesCode, resolveResult.StartLine, resolveResult.EndLine, resolveResult.IndentLength, resolveResult.ExcludesLines); + // Add the full file when no query option is given + return new DfmExtractCodeResult { IsSuccessful = true, FencesCodeLines = fencesCode }; } - else - { - // line range check only need to be done for line number representation - string errorMessage; - if (!CheckLineRange(fencesCode.Length, token.PathQueryOption?.StartLine, token.PathQueryOption?.EndLine, out errorMessage)) - { - Logger.LogError(errorMessage); - return new DfmExtractCodeResult { IsSuccessful = false, ErrorMessage = errorMessage, FencesCodeLines = fencesCode }; - } - int startLine = token.PathQueryOption?.StartLine ?? 1; - int endLine = token.PathQueryOption?.EndLine ?? fencesCode.Length; - int indentLength = (from line in fencesCode.Skip(startLine - 1).Take(endLine - startLine + 1) - where !string.IsNullOrEmpty(line) && !string.IsNullOrWhiteSpace(line) - select (int?)DfmCodeExtractorHelper.GetIndentLength(line)).Min() ?? 0; - return GetFencesCodeCore(fencesCode, startLine, endLine, indentLength); + if (!token.PathQueryOption.ValidateAndPrepare(fencesCode, token)) + { + Logger.LogError(token.PathQueryOption.ErrorMessage); + return new DfmExtractCodeResult { IsSuccessful = false, ErrorMessage = token.PathQueryOption.ErrorMessage, FencesCodeLines = fencesCode }; } - } - private DfmExtractCodeResult GetFencesCodeCore(string[] codeLines, int startLine, int endLine, int indentLength, HashSet<int> excludedLines = null) - { - long totalLines = codeLines.Length; var includedLines = new List<string>(); - for (int i = startLine; i <= Math.Min(endLine, totalLines); i++) + foreach (var line in token.PathQueryOption.GetQueryLines(fencesCode)) { - if (excludedLines == null || !excludedLines.Contains(i)) - { - includedLines.Add(codeLines[i - 1]); - } + includedLines.Add(line); } + int indentLength = (from line in includedLines + where !string.IsNullOrEmpty(line) && !string.IsNullOrWhiteSpace(line) + select (int?)DfmCodeExtractorHelper.GetIndentLength(line)).Min() ?? 0; return new DfmExtractCodeResult { IsSuccessful = true, FencesCodeLines = (indentLength == 0 ? includedLines : includedLines.Select(s => Regex.Replace(s, string.Format(RemoveIndentSpacesRegexString, indentLength), string.Empty))).ToArray() }; } - - private DfmTagNameResolveResult ResolveTagNamesFromPath(string fencesPath, string[] fencesCodeLines, string tagName, List<ICodeSnippetExtractor> codeSnippetExtractors) - { - var lazyResolveResults = - _dfmTagNameLineRangeCache.GetOrAdd(fencesPath, - path => new Lazy<ConcurrentDictionary<string, DfmTagNameResolveResult>>( - () => - { - // TODO: consider different code snippet representation with same name - return new ConcurrentDictionary<string, DfmTagNameResolveResult>( - (from codeSnippetExtractor in codeSnippetExtractors - let result = codeSnippetExtractor.GetAll(fencesCodeLines) - from codeSnippet in result - group codeSnippet by codeSnippet.Key).ToDictionary(d => d.Key, d => d.First().Value), StringComparer.OrdinalIgnoreCase); - })); - - DfmTagNameResolveResult resolveResult; - var tagNamesDictionary = lazyResolveResults.Value; - return (tagNamesDictionary.TryGetValue(tagName, out resolveResult) || tagNamesDictionary.TryGetValue($"snippet{tagName}", out resolveResult)) - ? resolveResult - : new DfmTagNameResolveResult { IsSuccessful = false, ErrorMessage = $"Tag name {tagName} is not found" }; - } - - private static bool CheckLineRange(int totalLines, int? startLine, int? endLine, out string errorMessage) - { - errorMessage = string.Empty; - - if (startLine <= 0 || endLine <= 0) - { - errorMessage = "Start/End line should be larger than zero"; - return false; - } - - if (startLine > endLine) - { - errorMessage = $"Start line {startLine} shouldn't be larger than end line {endLine}"; - return false; - } - - if (startLine > totalLines) - { - errorMessage = $"Start line '{startLine}' execeeds total file lines '{totalLines}'"; - return false; - } - - return true; - } - - private static string GetCodeLanguageOrExtension(DfmFencesBlockToken token) - { - return !string.IsNullOrEmpty(token.Lang) ? token.Lang : Path.GetExtension(token.Path); - } } } \ No newline at end of file diff --git a/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOption.cs b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOption.cs deleted file mode 100644 index 7c0c4418ee8..00000000000 --- a/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOption.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.DocAsCode.Dfm -{ - public class DfmFencesBlockPathQueryOption - { - public int? StartLine { get; set; } - - public int? EndLine { get; set; } - - public string TagName { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/DfmFencesBlockPathQueryOption.cs b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/DfmFencesBlockPathQueryOption.cs new file mode 100644 index 00000000000..1a217bdf4d2 --- /dev/null +++ b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/DfmFencesBlockPathQueryOption.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DocAsCode.Dfm +{ + using System.Collections.Generic; + + public abstract class DfmFencesBlockPathQueryOption : IDfmFencesBlockPathQueryOption + { + public string ErrorMessage { get; protected set; } + + public abstract bool ValidateAndPrepare(string[] lines, DfmFencesBlockToken token); + + public abstract IEnumerable<string> GetQueryLines(string[] lines); + + protected bool CheckLineRange(int totalLines, int? startLine, int? endLine) + { + if (startLine == null && endLine == null) + { + ErrorMessage = "Neither start line nor end line is specified correctly"; + return false; + } + + if (startLine <= 0 || endLine <= 0) + { + ErrorMessage = "Start/End line should be larger than zero"; + return false; + } + + if (startLine > endLine) + { + ErrorMessage = $"Start line {startLine} shouldn't be larger than end line {endLine}"; + return false; + } + + if (startLine > totalLines) + { + ErrorMessage = $"Start line '{startLine}' execeeds total file lines '{totalLines}'"; + return false; + } + + return true; + } + + } +} diff --git a/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/IDfmFencesBlockPathQueryOption.cs b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/IDfmFencesBlockPathQueryOption.cs new file mode 100644 index 00000000000..386bdc0f65f --- /dev/null +++ b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/IDfmFencesBlockPathQueryOption.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DocAsCode.Dfm +{ + using System.Collections.Generic; + + public interface IDfmFencesBlockPathQueryOption + { + string ErrorMessage { get; } + + bool ValidateAndPrepare(string[] lines, DfmFencesBlockToken token); + + IEnumerable<string> GetQueryLines(string[] lines); + } +} diff --git a/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/LineRangeBlockPathQueryOption.cs b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/LineRangeBlockPathQueryOption.cs new file mode 100644 index 00000000000..5a03ccdc0d3 --- /dev/null +++ b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/LineRangeBlockPathQueryOption.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DocAsCode.Dfm +{ + using System; + using System.Collections.Generic; + + class LineRangeBlockPathQueryOption : DfmFencesBlockPathQueryOption + { + public int? StartLine { get; set; } + + public int? EndLine { get; set; } + + public override bool ValidateAndPrepare(string[] lines, DfmFencesBlockToken token) + { + if (!CheckLineRange(lines.Length, StartLine, EndLine)) + { + return false; + } + + return true; + } + + public override IEnumerable<string> GetQueryLines(string[] lines) + { + int startLine = StartLine ?? 1; + int endLine = EndLine ?? lines.Length; + + for (int i = startLine; i <= Math.Min(endLine, lines.Length); i++) + { + yield return lines[i - 1]; + } + } + } +} diff --git a/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/MultipleLineRangeBlockPathQueryOption.cs b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/MultipleLineRangeBlockPathQueryOption.cs new file mode 100644 index 00000000000..81156a14f4a --- /dev/null +++ b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/MultipleLineRangeBlockPathQueryOption.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DocAsCode.Dfm +{ + using System; + using System.Collections.Generic; + + class MultipleLineRangeBlockPathQueryOption : DfmFencesBlockPathQueryOption + { + public List<Tuple<int?, int?>> LinePairs { get; set; } = new List<Tuple<int?, int?>>(); + + public override bool ValidateAndPrepare(string[] lines, DfmFencesBlockToken token) + { + foreach (var pair in LinePairs) + { + if (!CheckLineRange(lines.Length, pair.Item1, pair.Item2)) + { + return false; + } + } + + return true; + } + + public override IEnumerable<string> GetQueryLines(string[] lines) + { + foreach (var pair in LinePairs) + { + int startLine = pair.Item1 ?? 1; + int endLine = pair.Item2 ?? lines.Length; + + for (int i = startLine; i <= Math.Min(endLine, lines.Length); i++) + { + yield return lines[i - 1]; + } + } + } + } +} diff --git a/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/TagNameBlockPathQueryOption.cs b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/TagNameBlockPathQueryOption.cs new file mode 100644 index 00000000000..81115053fd7 --- /dev/null +++ b/src/Microsoft.DocAsCode.Dfm/DfmFencesBlockPathQueryOptions/TagNameBlockPathQueryOption.cs @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DocAsCode.Dfm +{ + using System; + using System.Collections.Generic; + using System.Collections.Concurrent; + using System.IO; + using System.Linq; + using System.Text.RegularExpressions; + + public class TagNameBlockPathQueryOption : DfmFencesBlockPathQueryOption + { + public string TagName { get; set; } + + // C# code snippet comment block: // <[/]snippetname> + private static readonly Regex CSharpCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex CSharpCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // C# code snippet region block: start -> #region snippetname, end -> #endregion + private static readonly Regex CSharpCodeSnippetRegionStartLineRegex = new Regex(@"^\s*#\s*region\s+(?<name>.+?)\s*$", RegexOptions.Compiled); + private static readonly Regex CSharpCodeSnippetRegionEndLineRegex = new Regex(@"^\s*#\s*endregion\s*$", RegexOptions.Compiled); + + // VB code snippet comment block: ' <[/]snippetname> + private static readonly Regex VBCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\'\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex VBCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\'\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // VB code snippet Region block: start -> # Region "snippetname", end -> # End Region + private static readonly Regex VBCodeSnippetRegionRegionStartLineRegex = new Regex(@"^\s*#\s*Region\s*(?<name>.+?)\s*$", RegexOptions.Compiled); + private static readonly Regex VBCodeSnippetRegionRegionEndLineRegex = new Regex(@"^\s*#\s*End\s+Region\s*$", RegexOptions.Compiled); + + // C++ code snippet block: // <[/]snippetname> + private static readonly Regex CPlusPlusCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex CPlusPlusCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // F# code snippet block: // <[/]snippetname> + private static readonly Regex FSharpCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex FSharpCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // XML code snippet block: <!-- <[/]snippetname> --> + private static readonly Regex XmlCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex XmlCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // XAML code snippet block: <!-- <[/]snippetname> --> + private static readonly Regex XamlCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex XamlCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // HTML code snippet block: <!-- <[/]snippetname> --> + private static readonly Regex HtmlCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex HtmlCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\<\!\-{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*\-{2}\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // Sql code snippet block: -- <[/]snippetname> + private static readonly Regex SqlCodeSnippetCommentStartLineRegex = new Regex(@"^\s*\-{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SqlCodeSnippetCommentEndLineRegex = new Regex(@"^\s*\-{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // Javascript code snippet block: <!-- <[/]snippetname> --> + private static readonly Regex JavaScriptSnippetCommentStartLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex JavaScriptSnippetCommentEndLineRegex = new Regex(@"^\s*\/{2}\s*\<\s*\/\s*(?<name>[\w\.]+)\s*\>\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // Language names and aliases fllow http://highlightjs.readthedocs.org/en/latest/css-classes-reference.html#language-names-and-aliases + // Language file extensions follow https://github.com/github/linguist/blob/master/lib/linguist/languages.yml + // Currently only supports parts of the language names, aliases and extensions + // Later we can move the repository's supported/custom language names, aliases, extensions and corresponding comments regexes to docfx build configuration + private static readonly IReadOnlyDictionary<string, List<ICodeSnippetExtractor>> CodeLanguageExtractors = + new Dictionary<string, List<ICodeSnippetExtractor>>(StringComparer.OrdinalIgnoreCase) + { + [".cs"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(CSharpCodeSnippetCommentStartLineRegex, CSharpCodeSnippetCommentEndLineRegex), + new RecursiveNameCodeSnippetExtractor(CSharpCodeSnippetRegionStartLineRegex, CSharpCodeSnippetRegionEndLineRegex) + }, + ["cs"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(CSharpCodeSnippetCommentStartLineRegex, CSharpCodeSnippetCommentEndLineRegex), + new RecursiveNameCodeSnippetExtractor(CSharpCodeSnippetRegionStartLineRegex, CSharpCodeSnippetRegionEndLineRegex) + }, + ["csharp"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(CSharpCodeSnippetCommentStartLineRegex, CSharpCodeSnippetCommentEndLineRegex), + new RecursiveNameCodeSnippetExtractor(CSharpCodeSnippetRegionStartLineRegex, CSharpCodeSnippetRegionEndLineRegex) + }, + [".vb"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(VBCodeSnippetCommentStartLineRegex, VBCodeSnippetCommentEndLineRegex), + new RecursiveNameCodeSnippetExtractor(VBCodeSnippetRegionRegionStartLineRegex, VBCodeSnippetRegionRegionEndLineRegex) + }, + ["vb"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(VBCodeSnippetCommentStartLineRegex, VBCodeSnippetCommentEndLineRegex), + new RecursiveNameCodeSnippetExtractor(VBCodeSnippetRegionRegionStartLineRegex, VBCodeSnippetRegionRegionEndLineRegex) + }, + ["vbnet"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(VBCodeSnippetCommentStartLineRegex, VBCodeSnippetCommentEndLineRegex), + new RecursiveNameCodeSnippetExtractor(VBCodeSnippetRegionRegionStartLineRegex, VBCodeSnippetRegionRegionEndLineRegex) + }, + [".cpp"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) + }, + [".h"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) + }, + [".hpp"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) + }, + [".c"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) + }, + [".cc"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) + }, + ["cpp"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) + }, + ["c++"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(CPlusPlusCodeSnippetCommentStartLineRegex, CPlusPlusCodeSnippetCommentEndLineRegex) + }, + ["fs"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(FSharpCodeSnippetCommentStartLineRegex, FSharpCodeSnippetCommentEndLineRegex) + }, + ["fsharp"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(FSharpCodeSnippetCommentStartLineRegex, FSharpCodeSnippetCommentEndLineRegex) + }, + [".fs"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(FSharpCodeSnippetCommentStartLineRegex, FSharpCodeSnippetCommentEndLineRegex) + }, + [".fsi"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(FSharpCodeSnippetCommentStartLineRegex, FSharpCodeSnippetCommentEndLineRegex) + }, + [".fsx"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(FSharpCodeSnippetCommentStartLineRegex, FSharpCodeSnippetCommentEndLineRegex) + }, + [".xml"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(XmlCodeSnippetCommentStartLineRegex, XmlCodeSnippetCommentEndLineRegex) + }, + [".csdl"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(XmlCodeSnippetCommentStartLineRegex, XmlCodeSnippetCommentEndLineRegex) + }, + [".edmx"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(XmlCodeSnippetCommentStartLineRegex, XmlCodeSnippetCommentEndLineRegex) + }, + ["xml"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(XmlCodeSnippetCommentStartLineRegex, XmlCodeSnippetCommentEndLineRegex) + }, + [".html"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(HtmlCodeSnippetCommentStartLineRegex, HtmlCodeSnippetCommentEndLineRegex) + }, + ["html"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(HtmlCodeSnippetCommentStartLineRegex, HtmlCodeSnippetCommentEndLineRegex) + }, + [".xaml"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(XamlCodeSnippetCommentStartLineRegex, XamlCodeSnippetCommentEndLineRegex) + }, + [".sql"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(SqlCodeSnippetCommentStartLineRegex, SqlCodeSnippetCommentEndLineRegex) + }, + ["sql"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(SqlCodeSnippetCommentStartLineRegex, SqlCodeSnippetCommentEndLineRegex) + }, + [".js"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(JavaScriptSnippetCommentStartLineRegex, JavaScriptSnippetCommentEndLineRegex) + }, + ["js"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(JavaScriptSnippetCommentStartLineRegex, JavaScriptSnippetCommentEndLineRegex) + }, + ["javascript"] = new List<ICodeSnippetExtractor> + { + new FlatNameCodeSnippetExtractor(JavaScriptSnippetCommentStartLineRegex, JavaScriptSnippetCommentEndLineRegex) + } + }; + + private DfmTagNameResolveResult resolveResult; + + private readonly ConcurrentDictionary<string, Lazy<ConcurrentDictionary<string, DfmTagNameResolveResult>>> _dfmTagNameLineRangeCache + = new ConcurrentDictionary<string, Lazy<ConcurrentDictionary<string, DfmTagNameResolveResult>>>(StringComparer.OrdinalIgnoreCase); + + public override bool ValidateAndPrepare(string[] lines, DfmFencesBlockToken token) + { + // NOTE: Parsing language and removing comment lines only do for tag name representation + var lang = GetCodeLanguageOrExtension(token); + List<ICodeSnippetExtractor> extractors; + if (!CodeLanguageExtractors.TryGetValue(lang, out extractors)) + { + ErrorMessage = $"{lang} is not supported languaging name, alias or extension for parsing code snippet with tag name, you can use line numbers instead"; + return false; + } + + resolveResult = ResolveTagNamesFromPath(token.Path, lines, TagName, extractors); + if (!resolveResult.IsSuccessful) + { + ErrorMessage = resolveResult.ErrorMessage; + return false; + } + + return true; + } + + public override IEnumerable<string> GetQueryLines(string[] lines) + { + for (int i = resolveResult.StartLine; i <= Math.Min(resolveResult.EndLine, lines.Length); i++) + { + if (resolveResult.ExcludesLines == null || !resolveResult.ExcludesLines.Contains(i)) + { + yield return lines[i - 1]; + } + } + } + + private DfmTagNameResolveResult ResolveTagNamesFromPath(string fencesPath, string[] fencesCodeLines, string tagName, List<ICodeSnippetExtractor> codeSnippetExtractors) + { + var lazyResolveResults = + _dfmTagNameLineRangeCache.GetOrAdd(fencesPath, + path => new Lazy<ConcurrentDictionary<string, DfmTagNameResolveResult>>( + () => + { + // TODO: consider different code snippet representation with same name + return new ConcurrentDictionary<string, DfmTagNameResolveResult>( + (from codeSnippetExtractor in codeSnippetExtractors + let result = codeSnippetExtractor.GetAll(fencesCodeLines) + from codeSnippet in result + group codeSnippet by codeSnippet.Key).ToDictionary(d => d.Key, d => d.First().Value), StringComparer.OrdinalIgnoreCase); + })); + + DfmTagNameResolveResult resolveResult; + var tagNamesDictionary = lazyResolveResults.Value; + return (tagNamesDictionary.TryGetValue(tagName, out resolveResult) || tagNamesDictionary.TryGetValue($"snippet{tagName}", out resolveResult)) + ? resolveResult + : new DfmTagNameResolveResult { IsSuccessful = false, ErrorMessage = $"Tag name {tagName} is not found" }; + } + + + private static string GetCodeLanguageOrExtension(DfmFencesBlockToken token) + { + return !string.IsNullOrEmpty(token.Lang) ? token.Lang : Path.GetExtension(token.Path); + } + } +} diff --git a/src/Microsoft.DocAsCode.Dfm/Microsoft.DocAsCode.Dfm.csproj b/src/Microsoft.DocAsCode.Dfm/Microsoft.DocAsCode.Dfm.csproj index 3b5f8769f1f..275dd574008 100644 --- a/src/Microsoft.DocAsCode.Dfm/Microsoft.DocAsCode.Dfm.csproj +++ b/src/Microsoft.DocAsCode.Dfm/Microsoft.DocAsCode.Dfm.csproj @@ -51,6 +51,11 @@ <Compile Include="CodeSnippetExtractor\FlatNameCodeSnippetExtractor.cs" /> <Compile Include="CodeSnippetExtractor\RecursiveNameCodeSnippetExtractor.cs" /> <Compile Include="DfmCodeExtractorHelper.cs" /> + <Compile Include="DfmFencesBlockPathQueryOptions\DfmFencesBlockPathQueryOption.cs" /> + <Compile Include="DfmFencesBlockPathQueryOptions\MultipleLineRangeBlockPathQueryOption.cs" /> + <Compile Include="DfmFencesBlockPathQueryOptions\TagNameBlockPathQueryOption.cs" /> + <Compile Include="DfmFencesBlockPathQueryOptions\LineRangeBlockPathQueryOption.cs" /> + <Compile Include="DfmFencesBlockPathQueryOptions\IDfmFencesBlockPathQueryOption.cs" /> <Compile Include="DfmMarkdownRenderer.cs" /> <Compile Include="CodeSnippetExtractor\ICodeSnippetExtractor.cs" /> <Compile Include="MarkdownValidators\MarkdownSytleConfig.cs" /> @@ -83,7 +88,6 @@ <Compile Include="DfmEngine.cs" /> <Compile Include="DfmEngineBuilder.cs" /> <Compile Include="DfmExtractCodeResult.cs" /> - <Compile Include="DfmFencesBlockPathQueryOption.cs" /> <Compile Include="DfmRenderer.cs" /> <Compile Include="DfmRendererHelper.cs" /> <Compile Include="DocfxFlavoredIncHelper.cs" /> diff --git a/src/Microsoft.DocAsCode.Dfm/Rules/DfmFencesBlockRule.cs b/src/Microsoft.DocAsCode.Dfm/Rules/DfmFencesBlockRule.cs index b12be5188f4..222b8cbdba0 100644 --- a/src/Microsoft.DocAsCode.Dfm/Rules/DfmFencesBlockRule.cs +++ b/src/Microsoft.DocAsCode.Dfm/Rules/DfmFencesBlockRule.cs @@ -3,7 +3,9 @@ namespace Microsoft.DocAsCode.Dfm { + using System; using System.Text.RegularExpressions; + using System.Collections.Generic; using System.Web; using Microsoft.DocAsCode.MarkdownLite; @@ -13,11 +15,14 @@ public class DfmFencesBlockRule : IMarkdownRule private const string StartLineQueryStringKey = "start"; private const string EndLineQueryStringKey = "end"; private const string TagNameQueryStringKey = "name"; + private const string RangeQueryStringKey = "range"; + private const char RegionSeparatorInRangeQueryString = ','; public string Name => "RestApiFences"; public static readonly Regex _dfmFencesRegex = new Regex(@"^\[\!((?i)code(\-(?<lang>[\w|\-]+))?)\s*\[(?<name>(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*)\]\(\s*<?(?<path>[\s\S]*?)((?<option>[\#|\?])(?<optionValue>\S+))?>?(?:\s+(?<quote>['""])(?<title>[\s\S]*?)\k<quote>)?\s*\)\]\s*(\n|$)", RegexOptions.Compiled); public static readonly Regex _dfmFencesSharpQueryStringRegex = new Regex(@"^L(?<start>\d+)\-L(?<end>\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static readonly Regex _dfmFencesRangeQueryStringRegex = new Regex(@"^(?<start>\d+)\-(?<end>\d+)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); public virtual IMarkdownToken TryMatch(IMarkdownParser engine, ref string source) { @@ -38,25 +43,25 @@ public virtual IMarkdownToken TryMatch(IMarkdownParser engine, ref string source return new DfmFencesBlockToken(this, engine.Context, name, path, match.Value, lang, title, pathQueryOption); } - private static DfmFencesBlockPathQueryOption ParsePathQueryString(string queryOption, string queryString) + private static IDfmFencesBlockPathQueryOption ParsePathQueryString(string queryOption, string queryString) { if (string.IsNullOrEmpty(queryOption) || string.IsNullOrEmpty(queryString)) { return null; } - int startLine, endLine; + int startLine, endLine, line; if (queryOption == "#") { // check if line number representation var match = _dfmFencesSharpQueryStringRegex.Match(queryString); if (match.Success && int.TryParse(match.Groups["start"].Value, out startLine) && int.TryParse(match.Groups["end"].Value, out endLine)) { - return new DfmFencesBlockPathQueryOption { StartLine = startLine, EndLine = endLine }; + return new LineRangeBlockPathQueryOption { StartLine = startLine, EndLine = endLine }; } else { - return new DfmFencesBlockPathQueryOption { TagName = queryString }; + return new TagNameBlockPathQueryOption { TagName = queryString }; } } else if (queryOption == "?") @@ -65,21 +70,49 @@ private static DfmFencesBlockPathQueryOption ParsePathQueryString(string queryOp var tagName = collection[TagNameQueryStringKey]; var start = collection[StartLineQueryStringKey]; var end = collection[EndLineQueryStringKey]; + var range = collection[RangeQueryStringKey]; if (tagName != null) { - return new DfmFencesBlockPathQueryOption { TagName = tagName }; + return new TagNameBlockPathQueryOption { TagName = tagName }; + } + else if (range != null) + { + var regions = range.Split(RegionSeparatorInRangeQueryString); + if (regions != null) + { + var option = new MultipleLineRangeBlockPathQueryOption(); + foreach (var region in regions) + { + var match = _dfmFencesRangeQueryStringRegex.Match(region); + if (match.Success) + { + // consider region as `{startlinenumber}-{endlinenumber}`, in which {endlinenumber} is optional + option.LinePairs.Add(new Tuple<int?, int?>( + int.TryParse(match.Groups["start"].Value, out startLine) ? startLine : (int?)null, + int.TryParse(match.Groups["end"].Value, out endLine) ? endLine : (int?)null + )); + } + else + { + // consider region as a sigine line number + var tempLine = int.TryParse(region, out line) ? line : (int?)null; + option.LinePairs.Add(new Tuple<int?, int?>(tempLine, tempLine)); + } + } + return option; + } } else if (start != null || end != null) { - return new DfmFencesBlockPathQueryOption + return new LineRangeBlockPathQueryOption { StartLine = int.TryParse(start, out startLine) ? startLine : (int?)null, EndLine = int.TryParse(end, out endLine) ? endLine : (int?)null }; } + return null; } - - return new DfmFencesBlockPathQueryOption(); + return null; } } } diff --git a/src/Microsoft.DocAsCode.Dfm/Tokens/DfmFencesBlockToken.cs b/src/Microsoft.DocAsCode.Dfm/Tokens/DfmFencesBlockToken.cs index a22e38b6ad2..9a7d3fee846 100644 --- a/src/Microsoft.DocAsCode.Dfm/Tokens/DfmFencesBlockToken.cs +++ b/src/Microsoft.DocAsCode.Dfm/Tokens/DfmFencesBlockToken.cs @@ -7,7 +7,7 @@ namespace Microsoft.DocAsCode.Dfm public class DfmFencesBlockToken : IMarkdownToken { - public DfmFencesBlockToken(IMarkdownRule rule, IMarkdownContext context, string name, string path, string rawMarkdown, string lang = null, string title = null, DfmFencesBlockPathQueryOption pathQueryOption = null) + public DfmFencesBlockToken(IMarkdownRule rule, IMarkdownContext context, string name, string path, string rawMarkdown, string lang = null, string title = null, IDfmFencesBlockPathQueryOption pathQueryOption = null) { Rule = rule; Context = context; @@ -31,7 +31,7 @@ public DfmFencesBlockToken(IMarkdownRule rule, IMarkdownContext context, string public string Title { get; } - public DfmFencesBlockPathQueryOption PathQueryOption { get; } + public IDfmFencesBlockPathQueryOption PathQueryOption { get; } public string RawMarkdown { get; set; } } diff --git a/test/Microsoft.DocAsCode.Dfm.Tests/DocfxFlavoredMarkdownTest.cs b/test/Microsoft.DocAsCode.Dfm.Tests/DocfxFlavoredMarkdownTest.cs index cfe4b2b100a..066ab42b3db 100644 --- a/test/Microsoft.DocAsCode.Dfm.Tests/DocfxFlavoredMarkdownTest.cs +++ b/test/Microsoft.DocAsCode.Dfm.Tests/DocfxFlavoredMarkdownTest.cs @@ -668,6 +668,27 @@ public static void Foo() { } } +</code></pre>")] + [InlineData(@"[!code[Main](Program.cs?range=1-2,10,20-21,29- ""This is root"")]", @"<pre><code name=""Main"" title=""This is root"">namespace ConsoleApplication1 +{ + class Program + #region Helper + internal static class Helper + #endregion +} +</code></pre>")] + [InlineData(@"[!code[Main](Program.cs?range=1,21,24-26,1,10,12-16 ""This is root"")]", @"<pre><code name=""Main"" title=""This is root"">namespace ConsoleApplication1 + internal static class Helper + public static void Foo() + { + } +namespace ConsoleApplication1 + class Program + static void Main(string[] args) + { + string s = "test"; + int i = 100; + } </code></pre>")] public void TestDfmFencesBlockLevelWithQueryString(string fencesPath, string expectedContent) {