diff --git a/Docnet.sln b/Docnet.sln index 8b375af..c463895 100644 --- a/Docnet.sln +++ b/Docnet.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docnet", "src\DocNet\Docnet.csproj", "{48CA9947-AF13-459E-9D59-FC451B5C19D7}" EndProject @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownDeep", "src\Markdow EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownDeepTests", "src\MarkdownDeepTests\MarkdownDeepTests.csproj", "{CD1F5BFF-0118-4994-86A2-92658A36CE1B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Projbook.Extension", "src\Projbook.Extension\Projbook.Extension.csproj", "{8338B756-0519-4D20-BA04-3A8F4839237A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {CD1F5BFF-0118-4994-86A2-92658A36CE1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD1F5BFF-0118-4994-86A2-92658A36CE1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD1F5BFF-0118-4994-86A2-92658A36CE1B}.Release|Any CPU.Build.0 = Release|Any CPU + {8338B756-0519-4D20-BA04-3A8F4839237A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8338B756-0519-4D20-BA04-3A8F4839237A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8338B756-0519-4D20-BA04-3A8F4839237A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8338B756-0519-4D20-BA04-3A8F4839237A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/DocNet/App.config b/src/DocNet/App.config index 731f6de..5893e9a 100644 --- a/src/DocNet/App.config +++ b/src/DocNet/App.config @@ -1,6 +1,18 @@ - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/DocNet/Docnet.csproj b/src/DocNet/Docnet.csproj index f083b6f..062811f 100644 --- a/src/DocNet/Docnet.csproj +++ b/src/DocNet/Docnet.csproj @@ -80,4 +80,4 @@ --> - + \ No newline at end of file diff --git a/src/MarkdownDeep/BlockProcessor.cs b/src/MarkdownDeep/BlockProcessor.cs index a307584..1dd9e2d 100644 --- a/src/MarkdownDeep/BlockProcessor.cs +++ b/src/MarkdownDeep/BlockProcessor.cs @@ -14,8 +14,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; +using Projbook.Extension; +using Projbook.Extension.CSharpExtractor; +using Projbook.Extension.Spi; +using Projbook.Extension.XmlExtractor; namespace MarkdownDeep { @@ -1281,23 +1286,26 @@ namespace MarkdownDeep { return HandleAlertExtension(b); } + if(DoesMatch("@snippet")) + { + return HandleSnippetExtension(b); + } return false; } - - /// - /// Handles the alert extension: - /// @alert type - /// text - /// @end - /// - /// where text can be anything and has to be handled further. - /// type is: danger, warning, info or neutral. - /// - /// The b. - /// - private bool HandleAlertExtension(Block b) + /// + /// Handles the alert extension: + /// @alert type + /// text + /// @end + /// + /// where text can be anything and has to be handled further. + /// type is: danger, warning, info or neutral. + /// + /// The b. + /// + private bool HandleAlertExtension(Block b) { // skip '@alert' if(!SkipString("@alert")) @@ -1438,15 +1446,114 @@ namespace MarkdownDeep b.Children = contentProcessor.ScanLines(Input, startContent, endContent - startContent); return true; } - - - /// - /// Handles the font awesome extension, which is available in DocNet mode. FontAwesome extension uses @fa-iconname, where iconname is the name of the fontawesome icon. - /// Called when '@fa-' has been seen. Current position is on 'f' of 'fa-'. - /// - /// The b. - /// - private bool HandleFontAwesomeExtension(Block b) + + + /// + /// Handles the snippet extension: + /// @snippet language [filename] pattern + /// + /// where 'language' can be: cs, xml or txt. If something else, txt is used + /// '[filename]' is evaluated relatively to the document location of the current document. + /// 'pattern' is the pattern passed to the extractor, which is determined based on the language. This is Projbook code. + /// + /// The read snippet is wrapped in a fenced code block with language as language marker, except for txt, which will get 'nohighlight'. + /// This fenced code block is then parsed again and that result is returned as b's data. + /// + /// The block to handle. + /// + private bool HandleSnippetExtension(Block b) + { + // skip @snippet + if(!SkipString("@snippet")) + { + return false; + } + if(!SkipLinespace()) + { + return false; + } + + // language + var language = string.Empty; + if(!SkipIdentifier(ref language)) + { + return false; + } + + if(!SkipLinespace()) + { + return false; + } + + // [filename] + if(!this.SkipChar('[')) + { + return false; + } + // mark start of filename string + this.Mark(); + if(!this.Find(']')) + { + return false; + } + string filename = this.Extract(); + if(string.IsNullOrWhiteSpace(filename)) + { + return false; + } + if(!SkipChar(']')) + { + return false; + } + if(!SkipLinespace()) + { + return false; + } + + // pattern + var patternStart = this.Position; + SkipToEol(); + var pattern = this.Input.Substring(patternStart, this.Position - patternStart); + SkipToNextLine(); + language = language.ToLowerInvariant(); + ISnippetExtractor extractor = null; + switch(language) + { + case "cs": + extractor = new CSharpSnippetExtractor(); + break; + case "xml": + extractor = new XmlSnippetExtractor(); + break; + default: + // text + language = "nohighlight"; + extractor = new DefaultSnippetExtractor(); + break; + } + + // extract the snippet, then build the fenced block to return. + var fullFilename = Path.Combine(Path.GetDirectoryName(m_markdown.DocumentLocation) ?? string.Empty, filename); + var snippetText = extractor.Extract(fullFilename, pattern) ?? string.Empty; + b.BlockType = BlockType.codeblock; + b.Data = language; + var child = CreateBlock(); + child.BlockType = BlockType.indent; + child.Buf = snippetText; + child.ContentStart = 0; + child.ContentEnd = snippetText.Length; + b.Children = new List() { child}; + return true; + } + + + /// + /// Handles the font awesome extension, which is available in DocNet mode. FontAwesome extension uses @fa-iconname, where iconname is the name of the fontawesome icon. + /// Called when '@fa-' has been seen. Current position is on 'f' of 'fa-'. + /// + /// The b. + /// + private bool HandleFontAwesomeExtension(Block b) { string iconName = string.Empty; int newPosition = this.Position; diff --git a/src/MarkdownDeep/MarkdownDeep.csproj b/src/MarkdownDeep/MarkdownDeep.csproj index aca5ccb..1fbb717 100644 --- a/src/MarkdownDeep/MarkdownDeep.csproj +++ b/src/MarkdownDeep/MarkdownDeep.csproj @@ -105,6 +105,12 @@ true + + + {8338b756-0519-4d20-ba04-3a8f4839237a} + Projbook.Extension + + diff --git a/src/MarkdownDeepTests/MarkdownDeepTests.csproj b/src/MarkdownDeepTests/MarkdownDeepTests.csproj index 69c5011..bfa5582 100644 --- a/src/MarkdownDeepTests/MarkdownDeepTests.csproj +++ b/src/MarkdownDeepTests/MarkdownDeepTests.csproj @@ -477,6 +477,12 @@ + + + + + + + \ No newline at end of file diff --git a/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpExtractionMode.cs b/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpExtractionMode.cs new file mode 100644 index 0000000..1cb6291 --- /dev/null +++ b/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpExtractionMode.cs @@ -0,0 +1,23 @@ +namespace Projbook.Extension.CSharpExtractor +{ + /// + /// Represents the extraction mode. + /// + public enum CSharpExtractionMode + { + /// + /// Full member: Do not process the snippet and print it as it. + /// + FullMember, + + /// + /// Content only: Extract the code block and print this part only. + /// + ContentOnly, + + /// + /// Block structure only: Remove the block content and print the code structure only. + /// + BlockStructureOnly + } +} \ No newline at end of file diff --git a/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpMatchingRule.cs b/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpMatchingRule.cs new file mode 100644 index 0000000..6d6c221 --- /dev/null +++ b/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpMatchingRule.cs @@ -0,0 +1,80 @@ +using Projbook.Extension.Exception; +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Projbook.Extension.CSharpExtractor +{ + /// + /// Represents a matching rule for referencing a C# member. + /// + public class CSharpMatchingRule + { + /// + /// The matching chunk to identify which member are the snippet targets. + /// + public string[] MatchingChunks { get; private set; } + + /// + /// The snippet extraction mode. + /// + public CSharpExtractionMode ExtractionMode { get; private set; } + + /// + /// Defines rule regex used to parse the snippet into chunks. + /// Expected input format: Path/File.cs [My.Name.Space.Class.Method][(string, string)] + /// * The first chunk is the file name and will be loaded in + /// * The optional second chunks are all full qualified name to the member separated by "." + /// * The optional last chunk is the method parameters if matching a method. + /// + private static Regex ruleRegex = new Regex(@"^([-=])?([^(]+)?\s*(\([^)]*\s*\))?\s*$", RegexOptions.Compiled); + + /// + /// Parses the token + /// + /// + /// + public static CSharpMatchingRule Parse(string pattern) + { + // Try to match the regex + pattern = Regex.Replace(pattern, @"\s", string.Empty); + Match match = CSharpMatchingRule.ruleRegex.Match(pattern); + if (!match.Success || string.IsNullOrWhiteSpace(match.Groups[0].Value)) + { + throw new SnippetExtractionException("Invalid extraction rule", pattern); + } + + // Retrieve values from the regex matching + string extractionOption = match.Groups[1].Value; + string rawMember = match.Groups[2].Value.Trim(); + string rawParameters = match.Groups[3].Value.Trim(); + + // Build The matching chunk with extracted data + List matchingChunks = new List(); + matchingChunks.AddRange(rawMember.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries)); + if (rawParameters.Length >= 1) + { + matchingChunks.Add(rawParameters); + } + + // Read extraction mode + CSharpExtractionMode extractionMode = CSharpExtractionMode.FullMember; + switch (extractionOption) + { + case "-": + extractionMode = CSharpExtractionMode.ContentOnly; + break; + case "=": + extractionMode = CSharpExtractionMode.BlockStructureOnly; + break; + } + + // Build the matching rule based on the regex matching + return new CSharpMatchingRule + { + MatchingChunks = matchingChunks.ToArray(), + ExtractionMode = extractionMode + }; + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpSnippetExtractor.cs b/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpSnippetExtractor.cs new file mode 100644 index 0000000..bca6ae1 --- /dev/null +++ b/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpSnippetExtractor.cs @@ -0,0 +1,368 @@ +using EnsureThat; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Projbook.Extension.Exception; +using Projbook.Extension.Spi; +using System; +using System.IO.Abstractions; +using System.Linq; +using System.Text; + +namespace Projbook.Extension.CSharpExtractor +{ + /// + /// Extractor in charge of browsing source directories. load file content and extract requested member. + /// + [Syntax(name: "csharp")] + public class CSharpSnippetExtractor : DefaultSnippetExtractor + { + /// + /// Represents the matching trie used for member matching. + /// Because of the cost of building the Trie, this value is lazy loaded and kept for future usages. + /// + private CSharpSyntaxMatchingNode syntaxTrie; + + /// + /// Extracts a snippet from a given rule pattern. + /// + /// The file system info. + /// The member pattern to extract. + /// The extracted snippet. + public override Model.Snippet Extract(FileSystemInfoBase fileSystemInfo, string memberPattern) + { + // Return the entire code if no member is specified + if (string.IsNullOrWhiteSpace(memberPattern)) + { + return base.Extract(fileSystemInfo, memberPattern); + } + + // Parse the matching rule from the pattern + CSharpMatchingRule rule = CSharpMatchingRule.Parse(memberPattern); + + // Load the trie for pattern matching + if (null == this.syntaxTrie) + { + // Load file content + string sourceCode = base.LoadFile(this.ConvertToFile(fileSystemInfo)); + + // Build a syntax tree from the source code + SyntaxTree tree = CSharpSyntaxTree.ParseText(sourceCode); + SyntaxNode root = tree.GetRoot(); + + // Visit the syntax tree for generating a Trie for pattern matching + CSharpSyntaxWalkerMatchingBuilder syntaxMatchingBuilder = new CSharpSyntaxWalkerMatchingBuilder(); + syntaxMatchingBuilder.Visit(root); + + // Retrieve the Trie root + this.syntaxTrie = syntaxMatchingBuilder.Root; + } + + // Match the rule from the syntax matching Trie + CSharpSyntaxMatchingNode matchingTrie = syntaxTrie.Match(rule.MatchingChunks); + if (null == matchingTrie) + { + throw new SnippetExtractionException("Cannot find member", memberPattern); + } + + // Build a snippet for extracted syntax nodes + return this.BuildSnippet(matchingTrie.MatchingSyntaxNodes, rule.ExtractionMode); + } + + /// + /// Builds a snippet from extracted syntax nodes. + /// + /// The exctracted nodes. + /// The extraction mode. + /// The built snippet. + private Model.Snippet BuildSnippet(SyntaxNode[] nodes, CSharpExtractionMode extractionMode) + { + // Data validation + Ensure.That(() => nodes).IsNotNull(); + Ensure.That(() => nodes).HasItems(); + + // Extract code from each snippets + StringBuilder stringBuilder = new StringBuilder(); + bool firstSnippet = true; + foreach (SyntaxNode node in nodes) + { + // Write line return between each snippet + if (!firstSnippet) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine(); + } + + // Write each snippet line + string[] lines = node.GetText().Lines.Select(x => x.ToString()).ToArray(); + int contentPosition = this.DetermineContentPosition(node); + this.WriteAndCleanupSnippet(stringBuilder, lines, extractionMode, contentPosition); + + // Flag the first snippet as false + firstSnippet = false; + } + + // Create the snippet from the exctracted code + return new Model.PlainTextSnippet(stringBuilder.ToString()); + } + + /// + /// Determines the content's block position depending on the node type. + /// + /// The node to extract the content position from. + /// The determined content position or 0 if not found. + private int DetermineContentPosition(SyntaxNode node) + { + // Data validation + Ensure.That(() => node).IsNotNull(); + + // Select the content node element depending on the node type + TextSpan? contentTextSpan = null; + switch (node.Kind()) + { + // Accessor list content + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.IndexerDeclaration: + case SyntaxKind.EventDeclaration: + AccessorListSyntax accessorList = node.DescendantNodes().OfType().FirstOrDefault(); + if (null != accessorList) + { + contentTextSpan = accessorList.FullSpan; + } + break; + + // Contains children + case SyntaxKind.NamespaceDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ClassDeclaration: + SyntaxToken token = node.ChildTokens().Where(x => x.Kind() == SyntaxKind.OpenBraceToken).FirstOrDefault(); + if (null != token) + { + contentTextSpan = token.FullSpan; + } + break; + + // Block content + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.DestructorDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.AddAccessorDeclaration: + case SyntaxKind.RemoveAccessorDeclaration: + BlockSyntax block = node.DescendantNodes().OfType().FirstOrDefault(); + if (null != block) + { + contentTextSpan = block.FullSpan; + } + break; + + // Not processed by projbook csharp extractor + default: + break; + } + + // Compute a line break insensitive position based on the fetched content text span if any is found + if (null != contentTextSpan) + { + int relativeTextSpanStart = contentTextSpan.Value.Start - node.FullSpan.Start; + return node + .ToFullString() + .Substring(0, relativeTextSpanStart) + .Replace("\r\n", "") + .Replace("\n", "").Length; + } + + // Otherwise return 0 as default value + return 0; + } + + /// + /// Writes and cleanup line snippets. + /// Snippets are moved out of their context, for this reason we need to trim lines aroung and remove a part of the indentation. + /// + /// The string builder used as output. + /// The lines to process. + /// The extraction mode. + /// The content position. + private void WriteAndCleanupSnippet(StringBuilder stringBuilder, string[] lines, CSharpExtractionMode extractionMode, int contentPosition) + { + // Data validation + Ensure.That(() => stringBuilder).IsNotNull(); + Ensure.That(() => lines).IsNotNull(); + + // Do not process if lines are empty + if (0 >= lines.Length) + { + return; + } + + // Compute the index of the first selected line + int startPos = 0; + int skippedCharNumber = 0; + if (CSharpExtractionMode.ContentOnly == extractionMode) + { + // Compute the content position index in the first processed line + int contentPositionFirstLineIndex = 0; + for (int totalLinePosition = 0; startPos < lines.Length; ++startPos) + { + // Compute the content position in the current line + string line = lines[startPos]; + int relativePosition = contentPosition - totalLinePosition; + int contentPositionInLine = relativePosition < line.Length ? relativePosition: -1; + + // In expected in the current line + if (contentPositionInLine >= 0) + { + // Look for the relative index in the current line + // Save the found index and break the iteration if any open bracket is found + int indexOf = line.IndexOf('{', contentPositionInLine); + if (0 <= indexOf) + { + contentPositionFirstLineIndex = indexOf; + break; + } + } + + // Move the total line position after the processed line + totalLinePosition += lines[startPos].Length; + } + + // Extract block code if any opening bracket has been found + if (startPos < lines.Length) + { + int openingBracketPos = lines[startPos].IndexOf('{', contentPositionFirstLineIndex); + if (openingBracketPos >= 0) + { + // Extract the code before the curly bracket + if (lines[startPos].Length > openingBracketPos) + { + lines[startPos] = lines[startPos].Substring(openingBracketPos + 1); + } + + // Skip the current line if empty + if (string.IsNullOrWhiteSpace(lines[startPos]) && lines.Length > 1 + startPos) + { + ++startPos; + } + } + } + } + else + { + // Skip leading whitespace lines and keep track of the amount of skipped char + for (; startPos < lines.Length; ++startPos) + { + // Break on non whitespace line + string line = lines[startPos]; + if (line.Trim().Length > 0) + { + break; + } + + // Record skipped char number + skippedCharNumber += line.Length; + } + } + + // Compute the index of the lastselected line + int endPos = -1 + lines.Length; + if (CSharpExtractionMode.ContentOnly == extractionMode) + { + for (; 0 <= endPos && !lines[endPos].ToString().Contains('}'); --endPos); + + // Extract block code if any closing bracket has been found + if (0 <= endPos) + { + int closingBracketPos = lines[endPos].IndexOf('}'); + if (closingBracketPos >= 0) + { + // Extract the code before the curly bracket + if (lines[endPos].Length > closingBracketPos) + lines[endPos] = lines[endPos].Substring(0, closingBracketPos).TrimEnd(); + } + + // Skip the current line if empty + if (string.IsNullOrWhiteSpace(lines[endPos]) && lines.Length > -1 + endPos) + { + --endPos; + } + } + } + else + { + for (; 0 <= endPos && lines[endPos].ToString().Trim().Length == 0; --endPos); + } + + // Compute the padding to remove for removing a part of the indentation + int leftPadding = int.MaxValue; + for (int i = startPos; i <= endPos; ++i) + { + // Ignore empty lines in the middle of the snippet + if (!string.IsNullOrWhiteSpace(lines[i])) + { + // Adjust the left padding with the available whitespace at the beginning of the line + leftPadding = Math.Min(leftPadding, lines[i].ToString().TakeWhile(Char.IsWhiteSpace).Count()); + } + } + + // Write selected lines to the string builder + bool firstLine = true; + for (int i = startPos; i <= endPos; ++i) + { + // Write line return between each line + if (!firstLine) + { + stringBuilder.AppendLine(); + } + + // Remove a part of the indentation padding + if (lines[i].Length > leftPadding) + { + string line = lines[i].Substring(leftPadding); + + // Process the snippet depending on the extraction mode + switch (extractionMode) + { + // Extract the block structure only + case CSharpExtractionMode.BlockStructureOnly: + + // Compute the content position in the current line + int relativePosition = contentPosition - skippedCharNumber; + int contentPositionInLine = relativePosition < line.Length + leftPadding ? relativePosition : -1; + + // Look for open bracket from the content position in line + int openingBracketPos = -1; + if (contentPositionInLine >= 0) + { + openingBracketPos = line.IndexOf('{', Math.Max(0, contentPositionInLine - leftPadding)); + } + + // Anonymize code content if an open bracket is found + if (openingBracketPos >= 0) + { + // Extract the code before the curly bracket + if (line.Length > openingBracketPos) + line = line.Substring(0, 1 + openingBracketPos); + + // Replace the content and close the block + line += string.Format("{0} // ...{0}}}", Environment.NewLine); + + // Stop the iteration + endPos = i; + } + break; + } + + // Append the line + stringBuilder.Append(line); + skippedCharNumber += lines[i].Length; + } + + // Flag the first line as false + firstLine = false; + } + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpSyntaxMatchingNode.cs b/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpSyntaxMatchingNode.cs new file mode 100644 index 0000000..9f1b33a --- /dev/null +++ b/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpSyntaxMatchingNode.cs @@ -0,0 +1,201 @@ +using EnsureThat; +using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Projbook.Extension.CSharpExtractor +{ + /// + /// Represents a syntax matching node. + /// Thie node is used to build a Trie representing possible matching. + /// Each node contians children and matching syntax nodes. + /// + public class CSharpSyntaxMatchingNode + { + /// + /// The public Matching SyntaxNodes. + /// + public SyntaxNode[] MatchingSyntaxNodes + { + get + { + // Return empty array id the nodes are empty + if (null == this.matchingSyntaxNodes) + { + return new SyntaxNode[0]; + } + + // Return the matching syntax nodes + return this.matchingSyntaxNodes.ToArray(); + } + } + + /// + /// The node's children. + /// + private Dictionary children; + + /// + /// The node's maching syntax node. + /// + private List matchingSyntaxNodes; + + /// + /// Finds a node from syntax chunk. + /// + /// The chunks to match. + /// + public CSharpSyntaxMatchingNode Match(string[] chunks) + { + // Data validation + Ensure.That(() => chunks).IsNotNull(); + + // Browse the Trie until finding a matching + CSharpSyntaxMatchingNode matchingNode = this; + foreach (string fragment in chunks) + { + // Could not find any matching + if (null == matchingNode.children || !matchingNode.children.TryGetValue(fragment, out matchingNode)) + { + return null; + } + } + + // Return the matching node + return matchingNode; + } + + /// + /// Lookup a node from children and return it. if the node doesn't exist, a new one will be created and added to the children. + /// + /// The node name. + /// The node matching the requested name. + public CSharpSyntaxMatchingNode EnsureNode(string name) + { + // Data validation + Ensure.That(() => name).IsNotNullOrWhiteSpace(); + + // Fetch a node from existing children and return it if any is found + CSharpSyntaxMatchingNode firstLevelNode; + if (null != this.children && this.children.TryGetValue(name, out firstLevelNode)) + { + return firstLevelNode; + } + + // Otherwise create a new node and return it + else + { + // Lazu create the dictionary for storing children + if (null == this.children) + { + this.children = new Dictionary(); + } + + // Assign and return the new node + return this.children[name] = new CSharpSyntaxMatchingNode(); + } + } + + /// + /// Adds a syntax node as matching node. + /// + /// + public void AddSyntaxNode(SyntaxNode node) + { + // Data validation + Ensure.That(() => node).IsNotNull(); + + // Lazy create the syntax node list + if (null == this.matchingSyntaxNodes) + { + this.matchingSyntaxNodes = new List(); + } + + // Add the node to the known matching node + this.matchingSyntaxNodes.Add(node); + } + + /// + /// Copies to a given node. + /// + /// The node wherer to copy. + /// The node name. + public void CopyTo(CSharpSyntaxMatchingNode targetNode, string name) + { + // Data validation + Ensure.That(() => name).IsNotNullOrWhiteSpace(); + Ensure.That(() => targetNode).IsNotNull(); + + // Ensure and retrieve a node the the copy + CSharpSyntaxMatchingNode newNode = targetNode.EnsureNode(name); + + // Add syntax node to the created node + if (null != this.matchingSyntaxNodes) + { + // Lazy create the syntax nodes + if (null == newNode.matchingSyntaxNodes) + { + newNode.matchingSyntaxNodes = new List(); + } + + // Merge syntax nodes + int[] indexes = newNode.matchingSyntaxNodes.Select(x => x.Span.Start).ToArray(); + newNode.matchingSyntaxNodes.AddRange(this.matchingSyntaxNodes.Where(x => !indexes.Contains(x.Span.Start))); + } + + // Recurse for applying copy to the children + if (null != this.children && this.children.Count > 0) + { + string[] childrenName = this.children.Keys.ToArray(); + foreach (string childName in childrenName) + { + this.children[childName].CopyTo(newNode, childName); + } + } + } + + /// + /// Overrides ToString to renger the internal Trie to a string. + /// + /// The rendered Trie as string. + public override string ToString() + { + StringBuilder strinbBuilder = new StringBuilder(); + this.Write(strinbBuilder, null, 0); + return strinbBuilder.ToString(); + } + + /// + /// Writes a node to a string builder and recurse to the children. + /// + /// The string builder used as output. + /// The node name. + /// The node level. + private void Write(StringBuilder stringBuilder, string name, int level) + { + // Print the node only if the name is not null in order to ignore the root node for more clarity + int nextLevel = level; + if (null != name) + { + ++nextLevel; + stringBuilder.AppendLine(string.Format("{0}{1}", new string('-', level), name)); + } + + // Print each matching syntax node + foreach (var matchingSyntaxNode in this.MatchingSyntaxNodes) + { + stringBuilder.AppendLine(string.Format("{0}[{1}]", new string('-', level), matchingSyntaxNode.GetType().Name)); + } + + // Recurse to children + if (null != this.children) + { + foreach (string childName in this.children.Keys) + { + this.children[childName].Write(stringBuilder, childName, nextLevel); + } + } + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpSyntaxWalkerMatchingBuilder.cs b/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpSyntaxWalkerMatchingBuilder.cs new file mode 100644 index 0000000..d6a72f1 --- /dev/null +++ b/src/Projbook.Extension.CSharpExtractor/Projbook/Extension/CSharp/CSharpSyntaxWalkerMatchingBuilder.cs @@ -0,0 +1,329 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Linq; + +namespace Projbook.Extension.CSharpExtractor +{ + /// + /// Implements a syntax walker generating a Trie for pattern matching. + /// + public class CSharpSyntaxWalkerMatchingBuilder : CSharpSyntaxWalker + { + /// + /// The current Trie root available from the outside. + /// + public CSharpSyntaxMatchingNode Root { get; private set; } + + /// + /// The Trie root referencing the root without any reference change. + /// + private CSharpSyntaxMatchingNode internalInvariantRoot; + + /// + /// Initializes a new instance of . + /// + public CSharpSyntaxWalkerMatchingBuilder() + { + this.internalInvariantRoot = new CSharpSyntaxMatchingNode(); + this.Root = this.internalInvariantRoot; + } + + /// + /// Visits a namespace declaration. + /// A namespace may be composed with different segment dot separated, each segment has to be represented by a different node. + /// However the syntax node is attached to the last node only. + /// + /// The namespace declaration node to visit. + public override void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node) + { + // Retrieve the namespace name and split segments + string name = node.Name.ToString(); + string[] namespaces = name.Split('.'); + + // Keep track of the initial node the restore the root after the visit + CSharpSyntaxMatchingNode initialNode = this.Root; + + // Browse all namespaces and generate intermediate node for each segment for the copy to the root + CSharpSyntaxMatchingNode firstNamespaceNode = null; + foreach (string currentNamespace in namespaces) + { + // Create the node and keep track of the first one + this.Root = this.Root.EnsureNode(currentNamespace); + if (null == firstNamespaceNode) + { + firstNamespaceNode = this.Root; + } + } + + // Add the syntax node the last segment + this.Root.AddSyntaxNode(node); + + // Triger member visiting + base.VisitNamespaceDeclaration(node); + + // Copy the generated sub tree to the Trie root + firstNamespaceNode.CopyTo(this.internalInvariantRoot, namespaces[0]); + + // Restore the initial root + this.Root = initialNode; + } + + /// + /// Visits a class declaration. + /// + /// The class declaration to visit. + public override void VisitClassDeclaration(ClassDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: node.TypeParameterList, + exctractName: n => node.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitClassDeclaration); + } + + /// + /// Visits an interface declaration. + /// + /// The class declaration to visit. + public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: node.TypeParameterList, + exctractName: n => node.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitInterfaceDeclaration); + } + + /// + /// Visits an enum declaration. + /// + /// The enum declaration to visit. + public override void VisitEnumDeclaration(EnumDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => node.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitEnumDeclaration); + } + + + /// + /// Visits an enum member declaration. + /// + /// The enum member declaration to visit. + public override void VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => node.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitEnumMemberDeclaration); + } + + /// + /// Visits a property declaration. + /// + /// The property declaration to visit. + public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => n.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitPropertyDeclaration); + } + + /// + /// Visits a field declaration. + /// + /// The field declaration to visit. + public override void VisitFieldDeclaration(FieldDeclarationSyntax node) + { + // Visit each variable declaration + foreach(VariableDeclaratorSyntax variableDeclarationSyntax in node.Declaration.Variables) + { + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => variableDeclarationSyntax.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitFieldDeclaration); + } + } + + /// + /// Visits an indexter declaration. + /// + /// The indexter declaration to visit. + public override void VisitIndexerDeclaration(IndexerDeclarationSyntax node) + { + // Compute suffix for representing generics + string memberName = string.Empty; + if (null != node.ParameterList) + { + memberName = string.Format( + "[{0}]", + string.Join(",", node.ParameterList.Parameters.Select(x => x.Type.ToString()))); + } + + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => memberName, + targetNode: n => n, + visit: base.VisitIndexerDeclaration); + } + + /// + /// Visits an event declaration. + /// + /// The event declaration to visit. + public override void VisitEventDeclaration(EventDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => n.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitEventDeclaration); + } + + /// + /// Visits an accessor declaration. + /// + /// The accessor declaration to visit. + public override void VisitAccessorDeclaration(AccessorDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => n.Keyword.ValueText, + targetNode: n => n, + visit: base.VisitAccessorDeclaration); + } + + /// + /// Visits a method declaration. + /// + /// The method declaration to visit. + public override void VisitMethodDeclaration(MethodDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: node.TypeParameterList, + exctractName: n => n.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitMethodDeclaration); + } + + /// + /// Visits a constructor declaration. + /// + /// The constructor declaration to visit. + public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => "", + targetNode: n => n, + visit: base.VisitConstructorDeclaration); + } + + /// + /// Visits a destructor declaration. + /// + /// The destructor declaration to visit. + public override void VisitDestructorDeclaration(DestructorDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => "", + targetNode: n => n, + visit: base.VisitDestructorDeclaration); + } + + /// + /// Visits parameter list. + /// + /// The parameter list to visit. + public override void VisitParameterList(ParameterListSyntax node) + { + // Skip parameter list when the parent is a lambda + if ( + SyntaxKind.SimpleLambdaExpression == node.Parent.Kind() || + SyntaxKind.ParenthesizedLambdaExpression == node.Parent.Kind()) + { + return; + } + + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => string.Format("({0})", string.Join(",", node.Parameters.Select(x => x.Type.ToString()))), + targetNode: n => n.Parent, + visit: base.VisitParameterList); + } + + /// + /// Visits a member. + /// + /// The syntax node type to visit. + /// The node to visit. + /// Extract the node name. + /// The type parameter list. + /// Resolved the target node. + /// Visit sub nodes. + private void Visit(T node, Func exctractName, TypeParameterListSyntax typeParameterList , Func targetNode, Action visit) where T : CSharpSyntaxNode + { + // Retrieve the accessor name + string name = exctractName(node); + + // Compute suffix for representing generics + if (null != typeParameterList) + { + name = string.Format( + "{0}{{{1}}}", + name, + string.Join(",", typeParameterList.Parameters.Select(x => x.ToString()))); + } + + // Keep track of the initial node the restore the root after the visit + CSharpSyntaxMatchingNode initialNode = this.Root; + + // Create and add the node + this.Root = this.Root.EnsureNode(name); + this.Root.AddSyntaxNode(targetNode(node)); + + // Trigger member visiting + visit(node); + + // Copy the class sub tree to the Trie root + this.Root.CopyTo(this.internalInvariantRoot, name); + + // Restore the initial root + this.Root = initialNode; + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension.CSharpExtractor/Properties/AssemblyInfo.cs b/src/Projbook.Extension.CSharpExtractor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d9a7916 --- /dev/null +++ b/src/Projbook.Extension.CSharpExtractor/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Projbook.Extension.CSharpExtractor")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Projbook.Extension.CSharpExtractor")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f5431901-29ac-46d4-a717-de2a9114e82d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Projbook.Extension.CSharpExtractor/packages.config b/src/Projbook.Extension.CSharpExtractor/packages.config new file mode 100644 index 0000000..ecf5663 --- /dev/null +++ b/src/Projbook.Extension.CSharpExtractor/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Projbook.Extension.XmlExtractor/Projbook.Extension.XmlExtractor.csproj b/src/Projbook.Extension.XmlExtractor/Projbook.Extension.XmlExtractor.csproj new file mode 100644 index 0000000..1e600b5 --- /dev/null +++ b/src/Projbook.Extension.XmlExtractor/Projbook.Extension.XmlExtractor.csproj @@ -0,0 +1,68 @@ + + + + + Debug + AnyCPU + {BC3E43EB-2263-49B4-883A-B720EDDF9298} + Library + Properties + Projbook.Extension.XmlExtractor + Projbook.Extension.XmlExtractor + v4.6.1 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + ..\packages\System.IO.Abstractions.2.0.0.136\lib\net40\System.IO.Abstractions.dll + True + + + + + + + + + + + + + + + {8338b756-0519-4d20-ba04-3a8f4839237a} + Projbook.Extension + + + + + + + + \ No newline at end of file diff --git a/src/Projbook.Extension.XmlExtractor/Projbook/Extension/Xml/XmlSnippetExtractor.cs b/src/Projbook.Extension.XmlExtractor/Projbook/Extension/Xml/XmlSnippetExtractor.cs new file mode 100644 index 0000000..71fd51a --- /dev/null +++ b/src/Projbook.Extension.XmlExtractor/Projbook/Extension/Xml/XmlSnippetExtractor.cs @@ -0,0 +1,159 @@ +using System; +using Projbook.Extension.Exception; +using Projbook.Extension.Spi; +using System.IO.Abstractions; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; + +namespace Projbook.Extension.XmlExtractor +{ + /// + /// Extractor in charge of browsing source directories. load file content and extract requested member. + /// + [Syntax(name: "xml")] + public class XmlSnippetExtractor : DefaultSnippetExtractor + { + /// + /// The regex extracting the document namespaces + /// + private Regex regex = new Regex(@"xmlns:([^=]+)=""([^""]*)""", RegexOptions.Compiled); + + /// + /// The lazy loaded xml document. + /// + private XmlDocument xmlDocument; + + /// + /// The lazy loaded namespace manager. + /// + private XmlNamespaceManager xmlNamespaceManager; + + /// + /// Extracts a snippet from a given rule pattern. + /// + /// The file system info. + /// The member pattern to extract. + /// The extracted snippet. + public override Extension.Model.Snippet Extract(FileSystemInfoBase fileSystemInfo, string memberPattern) + { + // Return the entire code if no member is specified + if (string.IsNullOrWhiteSpace(memberPattern)) + { + return base.Extract(fileSystemInfo, memberPattern); + } + + // Load the xml document for xpath execution + if (null == this.xmlDocument) + { + // Load file content + string sourceCode = base.LoadFile(this.ConvertToFile(fileSystemInfo)); + + // Remove default avoiding to define and use a prefix for the default namespace + // This is not strictly correct in a xml point of view but it's closest to most needs + sourceCode = Regex.Replace(sourceCode, @"xmlns\s*=\s*""[^""]*""", string.Empty); + + // Parse the file as xml + this.xmlDocument = new XmlDocument(); + try + { + // Initialize the document and the namespace manager + this.xmlDocument.LoadXml(sourceCode); + this.xmlNamespaceManager = new XmlNamespaceManager(this.xmlDocument.NameTable); + + // Match namespace declaration for filling the namespace manager + Match match = this.regex.Match(sourceCode); + while (match.Success) + { + // Collect prefix and namespace value + string prefix = match.Groups[1].Value.Trim(); + string ns = match.Groups[2].Value.Trim(); + + // Add namespace declaration to the namespace manager + xmlNamespaceManager.AddNamespace(prefix, ns); + + // Mode to the next matching + match = match.NextMatch(); + } + } + + // Throw an exception is the file is not loadable as xml document + catch (System.Exception exception) + { + throw new SnippetExtractionException("Cannot parse xml file", exception.Message); + } + } + + // Execute Xpath query + XmlNodeList xmlNodeList = null; + try + { + xmlNodeList = this.xmlDocument.SelectNodes(memberPattern, this.xmlNamespaceManager); + } + catch + { + throw new SnippetExtractionException("Invalid extraction rule", memberPattern); + } + + // Ensure we found a result + if (xmlNodeList.Count <= 0) + { + throw new SnippetExtractionException("Cannot find member", memberPattern); + } + + // Build a snippet for extracted nodes + return this.BuildSnippet(xmlNodeList); + } + + /// + /// Builds a snippet from xml node. + /// + /// The xml node list. + /// The built snippet. + private Extension.Model.Snippet BuildSnippet(XmlNodeList xmlNodeList) + { + // Data validation + if(xmlNodeList == null) + { + throw new ArgumentNullException(nameof(xmlNodeList)); + } + + // Extract code from each snippets + StringBuilder stringBuilder = new StringBuilder(); + bool firstSnippet = true; + for (int i = 0; i < xmlNodeList.Count; ++i) + { + // Get the current node + XmlNode node = xmlNodeList.Item(i); + + // Write line return between each snippet + if (!firstSnippet) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine(); + } + + // Write each snippet + XmlWriterSettings settings = new XmlWriterSettings(); + settings.Indent = true; + settings.OmitXmlDeclaration = true; + settings.NewLineOnAttributes = true; + using (XmlWriter xmlWriter = XmlWriter.Create(stringBuilder, settings)) + { + node.WriteTo(xmlWriter); + } + + // Flag the first snippet as false + firstSnippet = false; + } + + // Remove all generate namespace declaration + // This is produce some output lacking of namespace declaration but it's what is relevant for a xml document extraction + string output = stringBuilder.ToString(); + output = Regex.Replace(output, @" ?xmlns\s*(:[^=]+)?\s*=\s*""[^""]*""", string.Empty); + + // Create the snippet from the extracted code + return new Model.PlainTextSnippet(output); + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension.XmlExtractor/Properties/AssemblyInfo.cs b/src/Projbook.Extension.XmlExtractor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..35cf6c0 --- /dev/null +++ b/src/Projbook.Extension.XmlExtractor/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Projbook.Extension.XmlExtractor")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Projbook.Extension.XmlExtractor")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("bc3e43eb-2263-49b4-883a-b720eddf9298")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Projbook.Extension.XmlExtractor/packages.config b/src/Projbook.Extension.XmlExtractor/packages.config new file mode 100644 index 0000000..0ff97ab --- /dev/null +++ b/src/Projbook.Extension.XmlExtractor/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Projbook.Extension/DefaultSnippetExtractor.cs b/src/Projbook.Extension/DefaultSnippetExtractor.cs new file mode 100644 index 0000000..53aa9b1 --- /dev/null +++ b/src/Projbook.Extension/DefaultSnippetExtractor.cs @@ -0,0 +1,53 @@ +using System.Text; +using System; +using Projbook.Extension.Spi; +using System.IO; + +namespace Projbook.Extension +{ + /// + /// Extractor in charge of browsing source directories. load file content and extract requested member. + /// + public class DefaultSnippetExtractor : ISnippetExtractor + { + /// + /// File target type. + /// + public TargetType TargetType { get { return TargetType.File; } } + + /// + /// Extracts a snippet. + /// + /// The full filename (with path) to load and to extract the snippet from. + /// The extraction pattern, never used for this implementation. + /// + /// The extracted snippet. + /// + /// fileSystemInfo + public virtual string Extract(string fullFilename, string pattern) + { + if(string.IsNullOrEmpty(fullFilename)) + { + throw new ArgumentNullException(nameof(fullFilename)); + } + return this.LoadFile(fullFilename) ?? string.Empty; + } + + /// + /// Loads a file from the file name. + /// + /// The full filename. + /// + /// The file's content. + /// + /// fileInfo + protected string LoadFile(string fullFilename) + { + if(string.IsNullOrEmpty(fullFilename)) + { + throw new ArgumentNullException(nameof(fullFilename)); + } + return File.ReadAllText(fullFilename, Encoding.UTF8); + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension/Exception/SnippetExtractionException.cs b/src/Projbook.Extension/Exception/SnippetExtractionException.cs new file mode 100644 index 0000000..e1ded67 --- /dev/null +++ b/src/Projbook.Extension/Exception/SnippetExtractionException.cs @@ -0,0 +1,25 @@ +namespace Projbook.Extension.Exception +{ + /// + /// Represents a snippet extraction exception. + /// + public class SnippetExtractionException : System.Exception + { + /// + /// The pattern the exception is about. + /// + public string Pattern { get; private set; } + + /// + /// Initializes a new instance of . + /// + /// Initializes the required . + /// Initializes the required . + public SnippetExtractionException(string message, string pattern) + : base(message) + { + // Initialize + this.Pattern = pattern; + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension/Extractors/CSharp/CSharpExtractionMode.cs b/src/Projbook.Extension/Extractors/CSharp/CSharpExtractionMode.cs new file mode 100644 index 0000000..1cb6291 --- /dev/null +++ b/src/Projbook.Extension/Extractors/CSharp/CSharpExtractionMode.cs @@ -0,0 +1,23 @@ +namespace Projbook.Extension.CSharpExtractor +{ + /// + /// Represents the extraction mode. + /// + public enum CSharpExtractionMode + { + /// + /// Full member: Do not process the snippet and print it as it. + /// + FullMember, + + /// + /// Content only: Extract the code block and print this part only. + /// + ContentOnly, + + /// + /// Block structure only: Remove the block content and print the code structure only. + /// + BlockStructureOnly + } +} \ No newline at end of file diff --git a/src/Projbook.Extension/Extractors/CSharp/CSharpMatchingRule.cs b/src/Projbook.Extension/Extractors/CSharp/CSharpMatchingRule.cs new file mode 100644 index 0000000..6d6c221 --- /dev/null +++ b/src/Projbook.Extension/Extractors/CSharp/CSharpMatchingRule.cs @@ -0,0 +1,80 @@ +using Projbook.Extension.Exception; +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Projbook.Extension.CSharpExtractor +{ + /// + /// Represents a matching rule for referencing a C# member. + /// + public class CSharpMatchingRule + { + /// + /// The matching chunk to identify which member are the snippet targets. + /// + public string[] MatchingChunks { get; private set; } + + /// + /// The snippet extraction mode. + /// + public CSharpExtractionMode ExtractionMode { get; private set; } + + /// + /// Defines rule regex used to parse the snippet into chunks. + /// Expected input format: Path/File.cs [My.Name.Space.Class.Method][(string, string)] + /// * The first chunk is the file name and will be loaded in + /// * The optional second chunks are all full qualified name to the member separated by "." + /// * The optional last chunk is the method parameters if matching a method. + /// + private static Regex ruleRegex = new Regex(@"^([-=])?([^(]+)?\s*(\([^)]*\s*\))?\s*$", RegexOptions.Compiled); + + /// + /// Parses the token + /// + /// + /// + public static CSharpMatchingRule Parse(string pattern) + { + // Try to match the regex + pattern = Regex.Replace(pattern, @"\s", string.Empty); + Match match = CSharpMatchingRule.ruleRegex.Match(pattern); + if (!match.Success || string.IsNullOrWhiteSpace(match.Groups[0].Value)) + { + throw new SnippetExtractionException("Invalid extraction rule", pattern); + } + + // Retrieve values from the regex matching + string extractionOption = match.Groups[1].Value; + string rawMember = match.Groups[2].Value.Trim(); + string rawParameters = match.Groups[3].Value.Trim(); + + // Build The matching chunk with extracted data + List matchingChunks = new List(); + matchingChunks.AddRange(rawMember.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries)); + if (rawParameters.Length >= 1) + { + matchingChunks.Add(rawParameters); + } + + // Read extraction mode + CSharpExtractionMode extractionMode = CSharpExtractionMode.FullMember; + switch (extractionOption) + { + case "-": + extractionMode = CSharpExtractionMode.ContentOnly; + break; + case "=": + extractionMode = CSharpExtractionMode.BlockStructureOnly; + break; + } + + // Build the matching rule based on the regex matching + return new CSharpMatchingRule + { + MatchingChunks = matchingChunks.ToArray(), + ExtractionMode = extractionMode + }; + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension/Extractors/CSharp/CSharpSnippetExtractor.cs b/src/Projbook.Extension/Extractors/CSharp/CSharpSnippetExtractor.cs new file mode 100644 index 0000000..faaff55 --- /dev/null +++ b/src/Projbook.Extension/Extractors/CSharp/CSharpSnippetExtractor.cs @@ -0,0 +1,362 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Projbook.Extension.Exception; +using Projbook.Extension.Spi; +using System; +using System.Linq; +using System.Text; + +namespace Projbook.Extension.CSharpExtractor +{ + /// + /// Extractor in charge of browsing source directories. load file content and extract requested member. + /// + public class CSharpSnippetExtractor : DefaultSnippetExtractor + { + /// + /// Represents the matching trie used for member matching. + /// Because of the cost of building the Trie, this value is lazy loaded and kept for future usages. + /// + private CSharpSyntaxMatchingNode syntaxTrie; + + /// + /// Extracts a snippet from a given rule pattern. + /// + /// The full filename (with path) to load and to extract the snippet from. + /// The member pattern to extract. + /// The extracted snippet. + public override string Extract(string fullFilename, string memberPattern) + { + // Return the entire code if no member is specified + if (string.IsNullOrWhiteSpace(memberPattern)) + { + return base.Extract(fullFilename, memberPattern); + } + + // Parse the matching rule from the pattern + CSharpMatchingRule rule = CSharpMatchingRule.Parse(memberPattern); + + // Load the trie for pattern matching + if (null == this.syntaxTrie) + { + // Load file content + string sourceCode = this.LoadFile(fullFilename); + + // Build a syntax tree from the source code + SyntaxTree tree = CSharpSyntaxTree.ParseText(sourceCode); + SyntaxNode root = tree.GetRoot(); + + // Visit the syntax tree for generating a Trie for pattern matching + CSharpSyntaxWalkerMatchingBuilder syntaxMatchingBuilder = new CSharpSyntaxWalkerMatchingBuilder(); + syntaxMatchingBuilder.Visit(root); + + // Retrieve the Trie root + this.syntaxTrie = syntaxMatchingBuilder.Root; + } + + // Match the rule from the syntax matching Trie + CSharpSyntaxMatchingNode matchingTrie = syntaxTrie.Match(rule.MatchingChunks); + if (null == matchingTrie) + { + throw new SnippetExtractionException("Cannot find member", memberPattern); + } + + // Build a snippet for extracted syntax nodes + return this.BuildSnippet(matchingTrie.MatchingSyntaxNodes, rule.ExtractionMode); + } + + /// + /// Builds a snippet from extracted syntax nodes. + /// + /// The exctracted nodes. + /// The extraction mode. + /// The built snippet. + private string BuildSnippet(SyntaxNode[] nodes, CSharpExtractionMode extractionMode) + { + if(nodes == null || !nodes.Any()) + { + throw new ArgumentException("'nodes' is null or empty"); + } + // Extract code from each snippets + StringBuilder stringBuilder = new StringBuilder(); + bool firstSnippet = true; + foreach (SyntaxNode node in nodes) + { + // Write line return between each snippet + if (!firstSnippet) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine(); + } + + // Write each snippet line + string[] lines = node.GetText().Lines.Select(x => x.ToString()).ToArray(); + int contentPosition = this.DetermineContentPosition(node); + this.WriteAndCleanupSnippet(stringBuilder, lines, extractionMode, contentPosition); + + // Flag the first snippet as false + firstSnippet = false; + } + + // Create the snippet from the exctracted code + return stringBuilder.ToString(); + } + + /// + /// Determines the content's block position depending on the node type. + /// + /// The node to extract the content position from. + /// The determined content position or 0 if not found. + private int DetermineContentPosition(SyntaxNode node) + { + if(node == null) + { + throw new ArgumentNullException(nameof(node)); + } + // Select the content node element depending on the node type + TextSpan? contentTextSpan = null; + switch (node.Kind()) + { + // Accessor list content + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.IndexerDeclaration: + case SyntaxKind.EventDeclaration: + AccessorListSyntax accessorList = node.DescendantNodes().OfType().FirstOrDefault(); + if (null != accessorList) + { + contentTextSpan = accessorList.FullSpan; + } + break; + + // Contains children + case SyntaxKind.NamespaceDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ClassDeclaration: + SyntaxToken token = node.ChildTokens().FirstOrDefault(x => x.Kind() == SyntaxKind.OpenBraceToken); + if (null != token) + { + contentTextSpan = token.FullSpan; + } + break; + + // Block content + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.DestructorDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.AddAccessorDeclaration: + case SyntaxKind.RemoveAccessorDeclaration: + BlockSyntax block = node.DescendantNodes().OfType().FirstOrDefault(); + if (null != block) + { + contentTextSpan = block.FullSpan; + } + break; + + // Not processed by projbook csharp extractor + default: + break; + } + + // Compute a line break insensitive position based on the fetched content text span if any is found + if (null != contentTextSpan) + { + int relativeTextSpanStart = contentTextSpan.Value.Start - node.FullSpan.Start; + return node + .ToFullString() + .Substring(0, relativeTextSpanStart) + .Replace("\r\n", "") + .Replace("\n", "").Length; + } + + // Otherwise return 0 as default value + return 0; + } + + /// + /// Writes and cleanup line snippets. + /// Snippets are moved out of their context, for this reason we need to trim lines aroung and remove a part of the indentation. + /// + /// The string builder used as output. + /// The lines to process. + /// The extraction mode. + /// The content position. + private void WriteAndCleanupSnippet(StringBuilder stringBuilder, string[] lines, CSharpExtractionMode extractionMode, int contentPosition) + { + // Do not process if lines are empty + if (0 >= lines.Length) + { + return; + } + + // Compute the index of the first selected line + int startPos = 0; + int skippedCharNumber = 0; + if (CSharpExtractionMode.ContentOnly == extractionMode) + { + // Compute the content position index in the first processed line + int contentPositionFirstLineIndex = 0; + for (int totalLinePosition = 0; startPos < lines.Length; ++startPos) + { + // Compute the content position in the current line + string line = lines[startPos]; + int relativePosition = contentPosition - totalLinePosition; + int contentPositionInLine = relativePosition < line.Length ? relativePosition: -1; + + // In expected in the current line + if (contentPositionInLine >= 0) + { + // Look for the relative index in the current line + // Save the found index and break the iteration if any open bracket is found + int indexOf = line.IndexOf('{', contentPositionInLine); + if (0 <= indexOf) + { + contentPositionFirstLineIndex = indexOf; + break; + } + } + + // Move the total line position after the processed line + totalLinePosition += lines[startPos].Length; + } + + // Extract block code if any opening bracket has been found + if (startPos < lines.Length) + { + int openingBracketPos = lines[startPos].IndexOf('{', contentPositionFirstLineIndex); + if (openingBracketPos >= 0) + { + // Extract the code before the curly bracket + if (lines[startPos].Length > openingBracketPos) + { + lines[startPos] = lines[startPos].Substring(openingBracketPos + 1); + } + + // Skip the current line if empty + if (string.IsNullOrWhiteSpace(lines[startPos]) && lines.Length > 1 + startPos) + { + ++startPos; + } + } + } + } + else + { + // Skip leading whitespace lines and keep track of the amount of skipped char + for (; startPos < lines.Length; ++startPos) + { + // Break on non whitespace line + string line = lines[startPos]; + if (line.Trim().Length > 0) + { + break; + } + + // Record skipped char number + skippedCharNumber += line.Length; + } + } + + // Compute the index of the lastselected line + int endPos = -1 + lines.Length; + if (CSharpExtractionMode.ContentOnly == extractionMode) + { + for (; 0 <= endPos && !lines[endPos].ToString().Contains('}'); --endPos); + + // Extract block code if any closing bracket has been found + if (0 <= endPos) + { + int closingBracketPos = lines[endPos].IndexOf('}'); + if (closingBracketPos >= 0) + { + // Extract the code before the curly bracket + if (lines[endPos].Length > closingBracketPos) + lines[endPos] = lines[endPos].Substring(0, closingBracketPos).TrimEnd(); + } + + // Skip the current line if empty + if (string.IsNullOrWhiteSpace(lines[endPos]) && lines.Length > -1 + endPos) + { + --endPos; + } + } + } + else + { + for (; 0 <= endPos && lines[endPos].ToString().Trim().Length == 0; --endPos); + } + + // Compute the padding to remove for removing a part of the indentation + int leftPadding = int.MaxValue; + for (int i = startPos; i <= endPos; ++i) + { + // Ignore empty lines in the middle of the snippet + if (!string.IsNullOrWhiteSpace(lines[i])) + { + // Adjust the left padding with the available whitespace at the beginning of the line + leftPadding = Math.Min(leftPadding, lines[i].ToString().TakeWhile(Char.IsWhiteSpace).Count()); + } + } + + // Write selected lines to the string builder + bool firstLine = true; + for (int i = startPos; i <= endPos; ++i) + { + // Write line return between each line + if (!firstLine) + { + stringBuilder.AppendLine(); + } + + // Remove a part of the indentation padding + if (lines[i].Length > leftPadding) + { + string line = lines[i].Substring(leftPadding); + + // Process the snippet depending on the extraction mode + switch (extractionMode) + { + // Extract the block structure only + case CSharpExtractionMode.BlockStructureOnly: + + // Compute the content position in the current line + int relativePosition = contentPosition - skippedCharNumber; + int contentPositionInLine = relativePosition < line.Length + leftPadding ? relativePosition : -1; + + // Look for open bracket from the content position in line + int openingBracketPos = -1; + if (contentPositionInLine >= 0) + { + openingBracketPos = line.IndexOf('{', Math.Max(0, contentPositionInLine - leftPadding)); + } + + // Anonymize code content if an open bracket is found + if (openingBracketPos >= 0) + { + // Extract the code before the curly bracket + if (line.Length > openingBracketPos) + line = line.Substring(0, 1 + openingBracketPos); + + // Replace the content and close the block + line += string.Format("{0} // ...{0}}}", Environment.NewLine); + + // Stop the iteration + endPos = i; + } + break; + } + + // Append the line + stringBuilder.Append(line); + skippedCharNumber += lines[i].Length; + } + + // Flag the first line as false + firstLine = false; + } + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension/Extractors/CSharp/CSharpSyntaxMatchingNode.cs b/src/Projbook.Extension/Extractors/CSharp/CSharpSyntaxMatchingNode.cs new file mode 100644 index 0000000..065a8a6 --- /dev/null +++ b/src/Projbook.Extension/Extractors/CSharp/CSharpSyntaxMatchingNode.cs @@ -0,0 +1,207 @@ +using System; +using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Projbook.Extension.CSharpExtractor +{ + /// + /// Represents a syntax matching node. + /// Thie node is used to build a Trie representing possible matching. + /// Each node contians children and matching syntax nodes. + /// + public class CSharpSyntaxMatchingNode + { + /// + /// The public Matching SyntaxNodes. + /// + public SyntaxNode[] MatchingSyntaxNodes + { + get + { + // Return empty array id the nodes are empty + if (null == this.matchingSyntaxNodes) + { + return new SyntaxNode[0]; + } + + // Return the matching syntax nodes + return this.matchingSyntaxNodes.ToArray(); + } + } + + /// + /// The node's children. + /// + private Dictionary children; + + /// + /// The node's maching syntax node. + /// + private List matchingSyntaxNodes; + + /// + /// Finds a node from syntax chunk. + /// + /// The chunks to match. + /// + public CSharpSyntaxMatchingNode Match(string[] chunks) + { + if(chunks == null) + { + throw new ArgumentNullException(nameof(chunks)); + } + // Browse the Trie until finding a matching + CSharpSyntaxMatchingNode matchingNode = this; + foreach (string fragment in chunks) + { + // Could not find any matching + if (null == matchingNode.children || !matchingNode.children.TryGetValue(fragment, out matchingNode)) + { + return null; + } + } + + // Return the matching node + return matchingNode; + } + + /// + /// Lookup a node from children and return it. if the node doesn't exist, a new one will be created and added to the children. + /// + /// The node name. + /// The node matching the requested name. + public CSharpSyntaxMatchingNode EnsureNode(string name) + { + if(string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' is null or whitespace"); + } + + // Fetch a node from existing children and return it if any is found + CSharpSyntaxMatchingNode firstLevelNode; + if (null != this.children && this.children.TryGetValue(name, out firstLevelNode)) + { + return firstLevelNode; + } + + // Otherwise create a new node and return it + // Lazu create the dictionary for storing children + if (null == this.children) + { + this.children = new Dictionary(); + } + + // Assign and return the new node + return this.children[name] = new CSharpSyntaxMatchingNode(); + } + + /// + /// Adds a syntax node as matching node. + /// + /// + public void AddSyntaxNode(SyntaxNode node) + { + if(node == null) + { + throw new ArgumentNullException(nameof(node)); + } + // Lazy create the syntax node list + if (null == this.matchingSyntaxNodes) + { + this.matchingSyntaxNodes = new List(); + } + + // Add the node to the known matching node + this.matchingSyntaxNodes.Add(node); + } + + /// + /// Copies to a given node. + /// + /// The node wherer to copy. + /// The node name. + public void CopyTo(CSharpSyntaxMatchingNode targetNode, string name) + { + if(string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' is null or whitespace"); + } + if(targetNode == null) + { + throw new ArgumentNullException(nameof(targetNode)); + } + + // Ensure and retrieve a node the the copy + CSharpSyntaxMatchingNode newNode = targetNode.EnsureNode(name); + + // Add syntax node to the created node + if (null != this.matchingSyntaxNodes) + { + // Lazy create the syntax nodes + if (null == newNode.matchingSyntaxNodes) + { + newNode.matchingSyntaxNodes = new List(); + } + + // Merge syntax nodes + int[] indexes = newNode.matchingSyntaxNodes.Select(x => x.Span.Start).ToArray(); + newNode.matchingSyntaxNodes.AddRange(this.matchingSyntaxNodes.Where(x => !indexes.Contains(x.Span.Start))); + } + + // Recurse for applying copy to the children + if (null != this.children && this.children.Count > 0) + { + string[] childrenName = this.children.Keys.ToArray(); + foreach (string childName in childrenName) + { + this.children[childName].CopyTo(newNode, childName); + } + } + } + + /// + /// Overrides ToString to renger the internal Trie to a string. + /// + /// The rendered Trie as string. + public override string ToString() + { + StringBuilder strinbBuilder = new StringBuilder(); + this.Write(strinbBuilder, null, 0); + return strinbBuilder.ToString(); + } + + /// + /// Writes a node to a string builder and recurse to the children. + /// + /// The string builder used as output. + /// The node name. + /// The node level. + private void Write(StringBuilder stringBuilder, string name, int level) + { + // Print the node only if the name is not null in order to ignore the root node for more clarity + int nextLevel = level; + if (null != name) + { + ++nextLevel; + stringBuilder.AppendLine(string.Format("{0}{1}", new string('-', level), name)); + } + + // Print each matching syntax node + foreach (var matchingSyntaxNode in this.MatchingSyntaxNodes) + { + stringBuilder.AppendLine(string.Format("{0}[{1}]", new string('-', level), matchingSyntaxNode.GetType().Name)); + } + + // Recurse to children + if (null != this.children) + { + foreach (string childName in this.children.Keys) + { + this.children[childName].Write(stringBuilder, childName, nextLevel); + } + } + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension/Extractors/CSharp/CSharpSyntaxWalkerMatchingBuilder.cs b/src/Projbook.Extension/Extractors/CSharp/CSharpSyntaxWalkerMatchingBuilder.cs new file mode 100644 index 0000000..d6a72f1 --- /dev/null +++ b/src/Projbook.Extension/Extractors/CSharp/CSharpSyntaxWalkerMatchingBuilder.cs @@ -0,0 +1,329 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Linq; + +namespace Projbook.Extension.CSharpExtractor +{ + /// + /// Implements a syntax walker generating a Trie for pattern matching. + /// + public class CSharpSyntaxWalkerMatchingBuilder : CSharpSyntaxWalker + { + /// + /// The current Trie root available from the outside. + /// + public CSharpSyntaxMatchingNode Root { get; private set; } + + /// + /// The Trie root referencing the root without any reference change. + /// + private CSharpSyntaxMatchingNode internalInvariantRoot; + + /// + /// Initializes a new instance of . + /// + public CSharpSyntaxWalkerMatchingBuilder() + { + this.internalInvariantRoot = new CSharpSyntaxMatchingNode(); + this.Root = this.internalInvariantRoot; + } + + /// + /// Visits a namespace declaration. + /// A namespace may be composed with different segment dot separated, each segment has to be represented by a different node. + /// However the syntax node is attached to the last node only. + /// + /// The namespace declaration node to visit. + public override void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node) + { + // Retrieve the namespace name and split segments + string name = node.Name.ToString(); + string[] namespaces = name.Split('.'); + + // Keep track of the initial node the restore the root after the visit + CSharpSyntaxMatchingNode initialNode = this.Root; + + // Browse all namespaces and generate intermediate node for each segment for the copy to the root + CSharpSyntaxMatchingNode firstNamespaceNode = null; + foreach (string currentNamespace in namespaces) + { + // Create the node and keep track of the first one + this.Root = this.Root.EnsureNode(currentNamespace); + if (null == firstNamespaceNode) + { + firstNamespaceNode = this.Root; + } + } + + // Add the syntax node the last segment + this.Root.AddSyntaxNode(node); + + // Triger member visiting + base.VisitNamespaceDeclaration(node); + + // Copy the generated sub tree to the Trie root + firstNamespaceNode.CopyTo(this.internalInvariantRoot, namespaces[0]); + + // Restore the initial root + this.Root = initialNode; + } + + /// + /// Visits a class declaration. + /// + /// The class declaration to visit. + public override void VisitClassDeclaration(ClassDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: node.TypeParameterList, + exctractName: n => node.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitClassDeclaration); + } + + /// + /// Visits an interface declaration. + /// + /// The class declaration to visit. + public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: node.TypeParameterList, + exctractName: n => node.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitInterfaceDeclaration); + } + + /// + /// Visits an enum declaration. + /// + /// The enum declaration to visit. + public override void VisitEnumDeclaration(EnumDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => node.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitEnumDeclaration); + } + + + /// + /// Visits an enum member declaration. + /// + /// The enum member declaration to visit. + public override void VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => node.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitEnumMemberDeclaration); + } + + /// + /// Visits a property declaration. + /// + /// The property declaration to visit. + public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => n.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitPropertyDeclaration); + } + + /// + /// Visits a field declaration. + /// + /// The field declaration to visit. + public override void VisitFieldDeclaration(FieldDeclarationSyntax node) + { + // Visit each variable declaration + foreach(VariableDeclaratorSyntax variableDeclarationSyntax in node.Declaration.Variables) + { + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => variableDeclarationSyntax.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitFieldDeclaration); + } + } + + /// + /// Visits an indexter declaration. + /// + /// The indexter declaration to visit. + public override void VisitIndexerDeclaration(IndexerDeclarationSyntax node) + { + // Compute suffix for representing generics + string memberName = string.Empty; + if (null != node.ParameterList) + { + memberName = string.Format( + "[{0}]", + string.Join(",", node.ParameterList.Parameters.Select(x => x.Type.ToString()))); + } + + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => memberName, + targetNode: n => n, + visit: base.VisitIndexerDeclaration); + } + + /// + /// Visits an event declaration. + /// + /// The event declaration to visit. + public override void VisitEventDeclaration(EventDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => n.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitEventDeclaration); + } + + /// + /// Visits an accessor declaration. + /// + /// The accessor declaration to visit. + public override void VisitAccessorDeclaration(AccessorDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => n.Keyword.ValueText, + targetNode: n => n, + visit: base.VisitAccessorDeclaration); + } + + /// + /// Visits a method declaration. + /// + /// The method declaration to visit. + public override void VisitMethodDeclaration(MethodDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: node.TypeParameterList, + exctractName: n => n.Identifier.ValueText, + targetNode: n => n, + visit: base.VisitMethodDeclaration); + } + + /// + /// Visits a constructor declaration. + /// + /// The constructor declaration to visit. + public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => "", + targetNode: n => n, + visit: base.VisitConstructorDeclaration); + } + + /// + /// Visits a destructor declaration. + /// + /// The destructor declaration to visit. + public override void VisitDestructorDeclaration(DestructorDeclarationSyntax node) + { + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => "", + targetNode: n => n, + visit: base.VisitDestructorDeclaration); + } + + /// + /// Visits parameter list. + /// + /// The parameter list to visit. + public override void VisitParameterList(ParameterListSyntax node) + { + // Skip parameter list when the parent is a lambda + if ( + SyntaxKind.SimpleLambdaExpression == node.Parent.Kind() || + SyntaxKind.ParenthesizedLambdaExpression == node.Parent.Kind()) + { + return; + } + + // Visit + this.Visit( + node: node, + typeParameterList: null, + exctractName: n => string.Format("({0})", string.Join(",", node.Parameters.Select(x => x.Type.ToString()))), + targetNode: n => n.Parent, + visit: base.VisitParameterList); + } + + /// + /// Visits a member. + /// + /// The syntax node type to visit. + /// The node to visit. + /// Extract the node name. + /// The type parameter list. + /// Resolved the target node. + /// Visit sub nodes. + private void Visit(T node, Func exctractName, TypeParameterListSyntax typeParameterList , Func targetNode, Action visit) where T : CSharpSyntaxNode + { + // Retrieve the accessor name + string name = exctractName(node); + + // Compute suffix for representing generics + if (null != typeParameterList) + { + name = string.Format( + "{0}{{{1}}}", + name, + string.Join(",", typeParameterList.Parameters.Select(x => x.ToString()))); + } + + // Keep track of the initial node the restore the root after the visit + CSharpSyntaxMatchingNode initialNode = this.Root; + + // Create and add the node + this.Root = this.Root.EnsureNode(name); + this.Root.AddSyntaxNode(targetNode(node)); + + // Trigger member visiting + visit(node); + + // Copy the class sub tree to the Trie root + this.Root.CopyTo(this.internalInvariantRoot, name); + + // Restore the initial root + this.Root = initialNode; + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension/Extractors/Xml/XmlSnippetExtractor.cs b/src/Projbook.Extension/Extractors/Xml/XmlSnippetExtractor.cs new file mode 100644 index 0000000..7e0821a --- /dev/null +++ b/src/Projbook.Extension/Extractors/Xml/XmlSnippetExtractor.cs @@ -0,0 +1,162 @@ +using System; +using Projbook.Extension.Exception; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; + +namespace Projbook.Extension.XmlExtractor +{ + /// + /// Extractor in charge of browsing source directories. load file content and extract requested member. + /// + public class XmlSnippetExtractor : DefaultSnippetExtractor + { + /// + /// The regex extracting the document namespaces + /// + private Regex regex = new Regex(@"xmlns:([^=]+)=""([^""]*)""", RegexOptions.Compiled); + + /// + /// The lazy loaded xml document. + /// + private XmlDocument xmlDocument; + + /// + /// The lazy loaded namespace manager. + /// + private XmlNamespaceManager xmlNamespaceManager; + + /// + /// Extracts a snippet from a given rule pattern. + /// + /// The full filename (with path) to load and to extract the snippet from. + /// The member pattern to extract. + /// + /// The extracted snippet. + /// + /// + /// Cannot parse xml file + /// or + /// Invalid extraction rule + /// or + /// Cannot find member + /// + public override string Extract(string fullFilename, string memberPattern) + { + // Return the entire code if no member is specified + if (string.IsNullOrWhiteSpace(memberPattern)) + { + return base.Extract(fullFilename, memberPattern); + } + + // Load the xml document for xpath execution + if (null == this.xmlDocument) + { + // Load file content + string sourceCode = this.LoadFile(fullFilename); + + // Remove default avoiding to define and use a prefix for the default namespace + // This is not strictly correct in a xml point of view but it's closest to most needs + sourceCode = Regex.Replace(sourceCode, @"xmlns\s*=\s*""[^""]*""", string.Empty); + + // Parse the file as xml + this.xmlDocument = new XmlDocument(); + try + { + // Initialize the document and the namespace manager + this.xmlDocument.LoadXml(sourceCode); + this.xmlNamespaceManager = new XmlNamespaceManager(this.xmlDocument.NameTable); + + // Match namespace declaration for filling the namespace manager + Match match = this.regex.Match(sourceCode); + while (match.Success) + { + // Collect prefix and namespace value + string prefix = match.Groups[1].Value.Trim(); + string ns = match.Groups[2].Value.Trim(); + + // Add namespace declaration to the namespace manager + xmlNamespaceManager.AddNamespace(prefix, ns); + + // Mode to the next matching + match = match.NextMatch(); + } + } + + // Throw an exception is the file is not loadable as xml document + catch (System.Exception exception) + { + throw new SnippetExtractionException("Cannot parse xml file", exception.Message); + } + } + + // Execute Xpath query + XmlNodeList xmlNodeList = null; + try + { + xmlNodeList = this.xmlDocument.SelectNodes(memberPattern, this.xmlNamespaceManager); + } + catch + { + throw new SnippetExtractionException("Invalid extraction rule", memberPattern); + } + + // Ensure we found a result + if (xmlNodeList.Count <= 0) + { + throw new SnippetExtractionException("Cannot find member", memberPattern); + } + + // Build a snippet for extracted nodes + return this.BuildSnippet(xmlNodeList); + } + + /// + /// Builds a snippet from xml node. + /// + /// The xml node list. + /// The built snippet. + private string BuildSnippet(XmlNodeList xmlNodeList) + { + // Data validation + if(xmlNodeList == null) + { + throw new ArgumentNullException(nameof(xmlNodeList)); + } + + // Extract code from each snippets + StringBuilder stringBuilder = new StringBuilder(); + bool firstSnippet = true; + for (int i = 0; i < xmlNodeList.Count; ++i) + { + // Get the current node + XmlNode node = xmlNodeList.Item(i); + + // Write line return between each snippet + if (!firstSnippet) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine(); + } + + // Write each snippet + XmlWriterSettings settings = new XmlWriterSettings(); + settings.Indent = true; + settings.OmitXmlDeclaration = true; + settings.NewLineOnAttributes = true; + using (XmlWriter xmlWriter = XmlWriter.Create(stringBuilder, settings)) + { + node.WriteTo(xmlWriter); + } + + // Flag the first snippet as false + firstSnippet = false; + } + + // Remove all generate namespace declaration + // This is produce some output lacking of namespace declaration but it's what is relevant for a xml document extraction + string output = stringBuilder.ToString(); + return Regex.Replace(output, @" ?xmlns\s*(:[^=]+)?\s*=\s*""[^""]*""", string.Empty) ?? string.Empty; + } + } +} \ No newline at end of file diff --git a/src/Projbook.Extension/Projbook.Extension.csproj b/src/Projbook.Extension/Projbook.Extension.csproj new file mode 100644 index 0000000..c5757f9 --- /dev/null +++ b/src/Projbook.Extension/Projbook.Extension.csproj @@ -0,0 +1,90 @@ + + + + + Debug + AnyCPU + {8338B756-0519-4D20-BA04-3A8F4839237A} + Library + Properties + Projbook.Extension + Projbook.Extension + v4.5.1 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Microsoft.CodeAnalysis.Common.1.3.2\lib\net45\Microsoft.CodeAnalysis.dll + True + + + ..\..\packages\Microsoft.CodeAnalysis.CSharp.1.3.2\lib\net45\Microsoft.CodeAnalysis.CSharp.dll + True + + + + ..\..\packages\System.Collections.Immutable.1.1.37\lib\dotnet\System.Collections.Immutable.dll + True + + + + + ..\..\packages\System.Reflection.Metadata.1.2.0\lib\portable-net45+win8\System.Reflection.Metadata.dll + True + + + ..\..\packages\System.Threading.Thread.4.0.0\lib\net46\System.Threading.Thread.dll + True + + + + + + + + + + + + + + + + + + + + Designer + + + + + + + + + \ No newline at end of file diff --git a/src/Projbook.Extension/Properties/AssemblyInfo.cs b/src/Projbook.Extension/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1c887fb --- /dev/null +++ b/src/Projbook.Extension/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Projbook.Extension")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Projbook.Extension")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("8338b756-0519-4d20-ba04-3a8f4839237a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Projbook.Extension/Spi/ISnippetExtractor.cs b/src/Projbook.Extension/Spi/ISnippetExtractor.cs new file mode 100644 index 0000000..4f8ffd4 --- /dev/null +++ b/src/Projbook.Extension/Spi/ISnippetExtractor.cs @@ -0,0 +1,24 @@ + +namespace Projbook.Extension.Spi +{ + /// + /// Defines interface for snippet extractor. + /// + public interface ISnippetExtractor + { + /// + /// Defines the target type. + /// + TargetType TargetType { get; } + + /// + /// Extracts a snippet. + /// + /// The full filename (with path) to load and to extract the snippet from. + /// The extraction pattern. + /// + /// The extracted snippet as string. + /// + string Extract(string fullFilename, string pattern); + } +} \ No newline at end of file diff --git a/src/Projbook.Extension/Spi/TargetType.cs b/src/Projbook.Extension/Spi/TargetType.cs new file mode 100644 index 0000000..0456708 --- /dev/null +++ b/src/Projbook.Extension/Spi/TargetType.cs @@ -0,0 +1,23 @@ +namespace Projbook.Extension.Spi +{ + /// + /// Represents an extraction target. + /// + public enum TargetType + { + /// + /// Free text target, used by plugins extracting from free value. + /// + FreeText, + + /// + /// File target, used by plugins extracting from a file. + /// + File, + + /// + /// Folder target, ised bu plugins extracting from a folder. + /// + Folder + } +} \ No newline at end of file diff --git a/src/Projbook.Extension/app.config b/src/Projbook.Extension/app.config new file mode 100644 index 0000000..a11801f --- /dev/null +++ b/src/Projbook.Extension/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/Projbook.Extension/packages.config b/src/Projbook.Extension/packages.config new file mode 100644 index 0000000..b851084 --- /dev/null +++ b/src/Projbook.Extension/packages.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file