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