| @@ -1,7 +1,7 @@ | |||||
| | | ||||
| Microsoft Visual Studio Solution File, Format Version 12.00 | Microsoft Visual Studio Solution File, Format Version 12.00 | ||||
| # Visual Studio 14 | # Visual Studio 14 | ||||
| VisualStudioVersion = 14.0.24720.0 | |||||
| VisualStudioVersion = 14.0.25420.1 | |||||
| MinimumVisualStudioVersion = 10.0.40219.1 | MinimumVisualStudioVersion = 10.0.40219.1 | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docnet", "src\DocNet\Docnet.csproj", "{48CA9947-AF13-459E-9D59-FC451B5C19D7}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docnet", "src\DocNet\Docnet.csproj", "{48CA9947-AF13-459E-9D59-FC451B5C19D7}" | ||||
| EndProject | EndProject | ||||
| @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownDeep", "src\Markdow | |||||
| EndProject | EndProject | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownDeepTests", "src\MarkdownDeepTests\MarkdownDeepTests.csproj", "{CD1F5BFF-0118-4994-86A2-92658A36CE1B}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownDeepTests", "src\MarkdownDeepTests\MarkdownDeepTests.csproj", "{CD1F5BFF-0118-4994-86A2-92658A36CE1B}" | ||||
| EndProject | EndProject | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Projbook.Extension", "src\Projbook.Extension\Projbook.Extension.csproj", "{8338B756-0519-4D20-BA04-3A8F4839237A}" | |||||
| EndProject | |||||
| Global | Global | ||||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| Debug|Any CPU = Debug|Any CPU | 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}.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.ActiveCfg = Release|Any CPU | ||||
| {CD1F5BFF-0118-4994-86A2-92658A36CE1B}.Release|Any CPU.Build.0 = 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 | EndGlobalSection | ||||
| GlobalSection(SolutionProperties) = preSolution | GlobalSection(SolutionProperties) = preSolution | ||||
| HideSolutionNode = FALSE | HideSolutionNode = FALSE | ||||
| @@ -1,6 +1,18 @@ | |||||
| <?xml version="1.0" encoding="utf-8" ?> | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <configuration> | <configuration> | ||||
| <startup> | <startup> | ||||
| <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /> | <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /> | ||||
| </startup> | </startup> | ||||
| <runtime> | |||||
| <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> | |||||
| <dependentAssembly> | |||||
| <assemblyIdentity name="System.Reflection.Metadata" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> | |||||
| <bindingRedirect oldVersion="0.0.0.0-1.3.0.0" newVersion="1.3.0.0" /> | |||||
| </dependentAssembly> | |||||
| <dependentAssembly> | |||||
| <assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> | |||||
| <bindingRedirect oldVersion="0.0.0.0-1.2.0.0" newVersion="1.2.0.0" /> | |||||
| </dependentAssembly> | |||||
| </assemblyBinding> | |||||
| </runtime> | |||||
| </configuration> | </configuration> | ||||
| @@ -80,4 +80,4 @@ | |||||
| <Target Name="AfterBuild"> | <Target Name="AfterBuild"> | ||||
| </Target> | </Target> | ||||
| --> | --> | ||||
| </Project> | |||||
| </Project> | |||||
| @@ -14,8 +14,13 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.IO; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Text; | using System.Text; | ||||
| using Projbook.Extension; | |||||
| using Projbook.Extension.CSharpExtractor; | |||||
| using Projbook.Extension.Spi; | |||||
| using Projbook.Extension.XmlExtractor; | |||||
| namespace MarkdownDeep | namespace MarkdownDeep | ||||
| { | { | ||||
| @@ -1281,23 +1286,26 @@ namespace MarkdownDeep | |||||
| { | { | ||||
| return HandleAlertExtension(b); | return HandleAlertExtension(b); | ||||
| } | } | ||||
| if(DoesMatch("@snippet")) | |||||
| { | |||||
| return HandleSnippetExtension(b); | |||||
| } | |||||
| return false; | return false; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| /// <param name="b">The b.</param> | |||||
| /// <returns></returns> | |||||
| private bool HandleAlertExtension(Block b) | |||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| /// <param name="b">The b.</param> | |||||
| /// <returns></returns> | |||||
| private bool HandleAlertExtension(Block b) | |||||
| { | { | ||||
| // skip '@alert' | // skip '@alert' | ||||
| if(!SkipString("@alert")) | if(!SkipString("@alert")) | ||||
| @@ -1438,15 +1446,114 @@ namespace MarkdownDeep | |||||
| b.Children = contentProcessor.ScanLines(Input, startContent, endContent - startContent); | b.Children = contentProcessor.ScanLines(Input, startContent, endContent - startContent); | ||||
| return true; | return true; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// 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-'. | |||||
| /// </summary> | |||||
| /// <param name="b">The b.</param> | |||||
| /// <returns></returns> | |||||
| private bool HandleFontAwesomeExtension(Block b) | |||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| /// <param name="b">The block to handle.</param> | |||||
| /// <returns></returns> | |||||
| 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<Block>() { child}; | |||||
| return true; | |||||
| } | |||||
| /// <summary> | |||||
| /// 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-'. | |||||
| /// </summary> | |||||
| /// <param name="b">The b.</param> | |||||
| /// <returns></returns> | |||||
| private bool HandleFontAwesomeExtension(Block b) | |||||
| { | { | ||||
| string iconName = string.Empty; | string iconName = string.Empty; | ||||
| int newPosition = this.Position; | int newPosition = this.Position; | ||||
| @@ -105,6 +105,12 @@ | |||||
| <Install>true</Install> | <Install>true</Install> | ||||
| </BootstrapperPackage> | </BootstrapperPackage> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\Projbook.Extension\Projbook.Extension.csproj"> | |||||
| <Project>{8338b756-0519-4d20-ba04-3a8f4839237a}</Project> | |||||
| <Name>Projbook.Extension</Name> | |||||
| </ProjectReference> | |||||
| </ItemGroup> | |||||
| <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | ||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <PostBuildEvent> | <PostBuildEvent> | ||||
| @@ -477,6 +477,12 @@ | |||||
| <ItemGroup> | <ItemGroup> | ||||
| <EmbeddedResource Include="testfiles\mdtest11\Images%28Titled%29.html" /> | <EmbeddedResource Include="testfiles\mdtest11\Images%28Titled%29.html" /> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| <ItemGroup> | |||||
| <EmbeddedResource Include="testfiles\docnetmode\Snippet%28DocNetMode%29.html" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <EmbeddedResource Include="testfiles\docnetmode\Snippet%28DocNetMode%29.txt" /> | |||||
| </ItemGroup> | |||||
| <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | ||||
| <!-- To modify your build process, add your task inside one of the targets below and uncomment it. | <!-- To modify your build process, add your task inside one of the targets below and uncomment it. | ||||
| Other similar extension points exist, see Microsoft.Common.targets. | Other similar extension points exist, see Microsoft.Common.targets. | ||||
| @@ -132,6 +132,10 @@ namespace MarkdownDeepTests | |||||
| { | { | ||||
| md.HtmlClassTitledImages = "figure"; | md.HtmlClassTitledImages = "figure"; | ||||
| } | } | ||||
| if(md.DocNetMode) | |||||
| { | |||||
| md.GitHubCodeBlocks = true; | |||||
| } | |||||
| string actual = md.Transform(input); | string actual = md.Transform(input); | ||||
| string actual_clean = Utils.strip_redundant_whitespace(actual); | string actual_clean = Utils.strip_redundant_whitespace(actual); | ||||
| @@ -0,0 +1,21 @@ | |||||
| <p>pre</p> | |||||
| <p>This is some text with a snippet.</p> | |||||
| <pre><code class="cs">[SetUp] | |||||
| public void SetUp() | |||||
| { | |||||
| m = new Markdown(); | |||||
| m.AutoHeadingIDs = true; | |||||
| m.ExtraMode = true; | |||||
| } | |||||
| </code></pre> | |||||
| <pre><code class="cs">[Test] | |||||
| public void WithPunctuation() | |||||
| { | |||||
| // ... | |||||
| } | |||||
| </code></pre> | |||||
| <p>post</p> | |||||
| @@ -0,0 +1,9 @@ | |||||
| pre | |||||
| This is some text with a snippet. | |||||
| @snippet cs [..\..\AutoHeaderIDTests.cs] SetUp | |||||
| @snippet cs [..\..\AutoHeaderIDTests.cs] =WithPunctuation | |||||
| post | |||||
| @@ -0,0 +1,90 @@ | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
| <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> | |||||
| <PropertyGroup> | |||||
| <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | |||||
| <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | |||||
| <ProjectGuid>{F5431901-29AC-46D4-A717-DE2A9114E82D}</ProjectGuid> | |||||
| <OutputType>Library</OutputType> | |||||
| <AppDesignerFolder>Properties</AppDesignerFolder> | |||||
| <RootNamespace>Projbook.Extension.CSharpExtractor</RootNamespace> | |||||
| <AssemblyName>Projbook.Extension.CSharpExtractor</AssemblyName> | |||||
| <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> | |||||
| <FileAlignment>512</FileAlignment> | |||||
| <TargetFrameworkProfile /> | |||||
| </PropertyGroup> | |||||
| <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | |||||
| <DebugSymbols>true</DebugSymbols> | |||||
| <DebugType>full</DebugType> | |||||
| <Optimize>false</Optimize> | |||||
| <OutputPath>bin\Debug\</OutputPath> | |||||
| <DefineConstants>DEBUG;TRACE</DefineConstants> | |||||
| <ErrorReport>prompt</ErrorReport> | |||||
| <WarningLevel>4</WarningLevel> | |||||
| </PropertyGroup> | |||||
| <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | |||||
| <DebugType>pdbonly</DebugType> | |||||
| <Optimize>true</Optimize> | |||||
| <OutputPath>bin\Release\</OutputPath> | |||||
| <DefineConstants>TRACE</DefineConstants> | |||||
| <ErrorReport>prompt</ErrorReport> | |||||
| <WarningLevel>4</WarningLevel> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <Reference Include="Microsoft.CodeAnalysis, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |||||
| <HintPath>..\packages\Microsoft.CodeAnalysis.Common.1.3.2\lib\net45\Microsoft.CodeAnalysis.dll</HintPath> | |||||
| <Private>True</Private> | |||||
| </Reference> | |||||
| <Reference Include="Microsoft.CodeAnalysis.CSharp, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |||||
| <HintPath>..\packages\Microsoft.CodeAnalysis.CSharp.1.3.2\lib\net45\Microsoft.CodeAnalysis.CSharp.dll</HintPath> | |||||
| <Private>True</Private> | |||||
| </Reference> | |||||
| <Reference Include="System" /> | |||||
| <Reference Include="System.Collections.Immutable, Version=1.1.37.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||||
| <HintPath>..\packages\System.Collections.Immutable.1.1.37\lib\dotnet\System.Collections.Immutable.dll</HintPath> | |||||
| <Private>True</Private> | |||||
| </Reference> | |||||
| <Reference Include="System.Core" /> | |||||
| <Reference Include="System.Reflection.Metadata, Version=1.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||||
| <HintPath>..\packages\System.Reflection.Metadata.1.2.0\lib\portable-net45+win8\System.Reflection.Metadata.dll</HintPath> | |||||
| <Private>True</Private> | |||||
| </Reference> | |||||
| <Reference Include="System.Xml.Linq" /> | |||||
| <Reference Include="System.Data.DataSetExtensions" /> | |||||
| <Reference Include="Microsoft.CSharp" /> | |||||
| <Reference Include="System.Data" /> | |||||
| <Reference Include="System.Net.Http" /> | |||||
| <Reference Include="System.Xml" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <Compile Include="Projbook\Extension\CSharp\CSharpExtractionMode.cs" /> | |||||
| <Compile Include="Projbook\Extension\CSharp\CSharpMatchingRule.cs" /> | |||||
| <Compile Include="Projbook\Extension\CSharp\CSharpSnippetExtractor.cs" /> | |||||
| <Compile Include="Projbook\Extension\CSharp\CSharpSyntaxMatchingNode.cs" /> | |||||
| <Compile Include="Projbook\Extension\CSharp\CSharpSyntaxWalkerMatchingBuilder.cs" /> | |||||
| <Compile Include="Properties\AssemblyInfo.cs" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\Projbook.Extension\Projbook.Extension.csproj"> | |||||
| <Project>{8338b756-0519-4d20-ba04-3a8f4839237a}</Project> | |||||
| <Name>Projbook.Extension</Name> | |||||
| </ProjectReference> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <None Include="packages.config"> | |||||
| <SubType>Designer</SubType> | |||||
| </None> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <Analyzer Include="..\packages\Microsoft.CodeAnalysis.Analyzers.1.1.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.Analyzers.dll" /> | |||||
| <Analyzer Include="..\packages\Microsoft.CodeAnalysis.Analyzers.1.1.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.CSharp.Analyzers.dll" /> | |||||
| </ItemGroup> | |||||
| <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | |||||
| <!-- To modify your build process, add your task inside one of the targets below and uncomment it. | |||||
| Other similar extension points exist, see Microsoft.Common.targets. | |||||
| <Target Name="BeforeBuild"> | |||||
| </Target> | |||||
| <Target Name="AfterBuild"> | |||||
| </Target> | |||||
| --> | |||||
| </Project> | |||||
| @@ -0,0 +1,23 @@ | |||||
| namespace Projbook.Extension.CSharpExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents the extraction mode. | |||||
| /// </summary> | |||||
| public enum CSharpExtractionMode | |||||
| { | |||||
| /// <summary> | |||||
| /// Full member: Do not process the snippet and print it as it. | |||||
| /// </summary> | |||||
| FullMember, | |||||
| /// <summary> | |||||
| /// Content only: Extract the code block and print this part only. | |||||
| /// </summary> | |||||
| ContentOnly, | |||||
| /// <summary> | |||||
| /// Block structure only: Remove the block content and print the code structure only. | |||||
| /// </summary> | |||||
| BlockStructureOnly | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,80 @@ | |||||
| using Projbook.Extension.Exception; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Text.RegularExpressions; | |||||
| namespace Projbook.Extension.CSharpExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents a matching rule for referencing a C# member. | |||||
| /// </summary> | |||||
| public class CSharpMatchingRule | |||||
| { | |||||
| /// <summary> | |||||
| /// The matching chunk to identify which member are the snippet targets. | |||||
| /// </summary> | |||||
| public string[] MatchingChunks { get; private set; } | |||||
| /// <summary> | |||||
| /// The snippet extraction mode. | |||||
| /// </summary> | |||||
| public CSharpExtractionMode ExtractionMode { get; private set; } | |||||
| /// <summary> | |||||
| /// 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 <seealso cref="TargetFile"/> | |||||
| /// * 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. | |||||
| /// </summary> | |||||
| private static Regex ruleRegex = new Regex(@"^([-=])?([^(]+)?\s*(\([^)]*\s*\))?\s*$", RegexOptions.Compiled); | |||||
| /// <summary> | |||||
| /// Parses the token | |||||
| /// </summary> | |||||
| /// <param name="pattern"></param> | |||||
| /// <returns></returns> | |||||
| 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<string> matchingChunks = new List<string>(); | |||||
| 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 | |||||
| }; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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 | |||||
| { | |||||
| /// <summary> | |||||
| /// Extractor in charge of browsing source directories. load file content and extract requested member. | |||||
| /// </summary> | |||||
| [Syntax(name: "csharp")] | |||||
| public class CSharpSnippetExtractor : DefaultSnippetExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| private CSharpSyntaxMatchingNode syntaxTrie; | |||||
| /// <summary> | |||||
| /// Extracts a snippet from a given rule pattern. | |||||
| /// </summary> | |||||
| /// <param name="fileSystemInfo">The file system info.</param> | |||||
| /// <param name="memberPattern">The member pattern to extract.</param> | |||||
| /// <returns>The extracted snippet.</returns> | |||||
| 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); | |||||
| } | |||||
| /// <summary> | |||||
| /// Builds a snippet from extracted syntax nodes. | |||||
| /// </summary> | |||||
| /// <param name="nodes">The exctracted nodes.</param> | |||||
| /// <param name="extractionMode">The extraction mode.</param> | |||||
| /// <returns>The built snippet.</returns> | |||||
| 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()); | |||||
| } | |||||
| /// <summary> | |||||
| /// Determines the content's block position depending on the node type. | |||||
| /// </summary> | |||||
| /// <param name="node">The node to extract the content position from.</param> | |||||
| /// <returns>The determined content position or 0 if not found.</returns> | |||||
| 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<AccessorListSyntax>().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<BlockSyntax>().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; | |||||
| } | |||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| /// <param name="stringBuilder">The string builder used as output.</param> | |||||
| /// <param name="lines">The lines to process.</param> | |||||
| /// <param name="extractionMode">The extraction mode.</param> | |||||
| /// <param name="contentPosition">The content position.</param> | |||||
| 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,201 @@ | |||||
| using EnsureThat; | |||||
| using Microsoft.CodeAnalysis; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| namespace Projbook.Extension.CSharpExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents a syntax matching node. | |||||
| /// Thie node is used to build a Trie representing possible matching. | |||||
| /// Each node contians children and matching syntax nodes. | |||||
| /// </summary> | |||||
| public class CSharpSyntaxMatchingNode | |||||
| { | |||||
| /// <summary> | |||||
| /// The public Matching SyntaxNodes. | |||||
| /// </summary> | |||||
| 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(); | |||||
| } | |||||
| } | |||||
| /// <summary> | |||||
| /// The node's children. | |||||
| /// </summary> | |||||
| private Dictionary<string, CSharpSyntaxMatchingNode> children; | |||||
| /// <summary> | |||||
| /// The node's maching syntax node. | |||||
| /// </summary> | |||||
| private List<SyntaxNode> matchingSyntaxNodes; | |||||
| /// <summary> | |||||
| /// Finds a node from syntax chunk. | |||||
| /// </summary> | |||||
| /// <param name="chunks">The chunks to match.</param> | |||||
| /// <returns></returns> | |||||
| 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; | |||||
| } | |||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| /// <param name="name">The node name.</param> | |||||
| /// <returns>The node matching the requested name.</returns> | |||||
| 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<string, CSharpSyntaxMatchingNode>(); | |||||
| } | |||||
| // Assign and return the new node | |||||
| return this.children[name] = new CSharpSyntaxMatchingNode(); | |||||
| } | |||||
| } | |||||
| /// <summary> | |||||
| /// Adds a syntax node as matching node. | |||||
| /// </summary> | |||||
| /// <param name="node"></param> | |||||
| 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<SyntaxNode>(); | |||||
| } | |||||
| // Add the node to the known matching node | |||||
| this.matchingSyntaxNodes.Add(node); | |||||
| } | |||||
| /// <summary> | |||||
| /// Copies to a given node. | |||||
| /// </summary> | |||||
| /// <param name="targetNode">The node wherer to copy.</param> | |||||
| /// <param name="name">The node name.</param> | |||||
| 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<SyntaxNode>(); | |||||
| } | |||||
| // 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| /// <summary> | |||||
| /// Overrides ToString to renger the internal Trie to a string. | |||||
| /// </summary> | |||||
| /// <returns>The rendered Trie as string.</returns> | |||||
| public override string ToString() | |||||
| { | |||||
| StringBuilder strinbBuilder = new StringBuilder(); | |||||
| this.Write(strinbBuilder, null, 0); | |||||
| return strinbBuilder.ToString(); | |||||
| } | |||||
| /// <summary> | |||||
| /// Writes a node to a string builder and recurse to the children. | |||||
| /// </summary> | |||||
| /// <param name="stringBuilder">The string builder used as output.</param> | |||||
| /// <param name="name">The node name.</param> | |||||
| /// <param name="level">The node level.</param> | |||||
| 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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 | |||||
| { | |||||
| /// <summary> | |||||
| /// Implements a syntax walker generating a Trie for pattern matching. | |||||
| /// </summary> | |||||
| public class CSharpSyntaxWalkerMatchingBuilder : CSharpSyntaxWalker | |||||
| { | |||||
| /// <summary> | |||||
| /// The current Trie root available from the outside. | |||||
| /// </summary> | |||||
| public CSharpSyntaxMatchingNode Root { get; private set; } | |||||
| /// <summary> | |||||
| /// The Trie root referencing the root without any reference change. | |||||
| /// </summary> | |||||
| private CSharpSyntaxMatchingNode internalInvariantRoot; | |||||
| /// <summary> | |||||
| /// Initializes a new instance of <see cref="CSharpSyntaxWalkerMatchingBuilder"/>. | |||||
| /// </summary> | |||||
| public CSharpSyntaxWalkerMatchingBuilder() | |||||
| { | |||||
| this.internalInvariantRoot = new CSharpSyntaxMatchingNode(); | |||||
| this.Root = this.internalInvariantRoot; | |||||
| } | |||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| /// <param name="node">The namespace declaration node to visit.</param> | |||||
| 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; | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a class declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The class declaration to visit.</param> | |||||
| public override void VisitClassDeclaration(ClassDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<ClassDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: node.TypeParameterList, | |||||
| exctractName: n => node.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitClassDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an interface declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The class declaration to visit.</param> | |||||
| public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<InterfaceDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: node.TypeParameterList, | |||||
| exctractName: n => node.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitInterfaceDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an enum declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The enum declaration to visit.</param> | |||||
| public override void VisitEnumDeclaration(EnumDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<EnumDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => node.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitEnumDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an enum member declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The enum member declaration to visit.</param> | |||||
| public override void VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<EnumMemberDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => node.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitEnumMemberDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a property declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The property declaration to visit.</param> | |||||
| public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<PropertyDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => n.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitPropertyDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a field declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The field declaration to visit.</param> | |||||
| public override void VisitFieldDeclaration(FieldDeclarationSyntax node) | |||||
| { | |||||
| // Visit each variable declaration | |||||
| foreach(VariableDeclaratorSyntax variableDeclarationSyntax in node.Declaration.Variables) | |||||
| { | |||||
| this.Visit<FieldDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => variableDeclarationSyntax.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitFieldDeclaration); | |||||
| } | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an indexter declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The indexter declaration to visit.</param> | |||||
| 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<IndexerDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => memberName, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitIndexerDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an event declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The event declaration to visit.</param> | |||||
| public override void VisitEventDeclaration(EventDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<EventDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => n.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitEventDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an accessor declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The accessor declaration to visit.</param> | |||||
| public override void VisitAccessorDeclaration(AccessorDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<AccessorDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => n.Keyword.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitAccessorDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a method declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The method declaration to visit.</param> | |||||
| public override void VisitMethodDeclaration(MethodDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<MethodDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: node.TypeParameterList, | |||||
| exctractName: n => n.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitMethodDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a constructor declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The constructor declaration to visit.</param> | |||||
| public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<ConstructorDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => "<Constructor>", | |||||
| targetNode: n => n, | |||||
| visit: base.VisitConstructorDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a destructor declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The destructor declaration to visit.</param> | |||||
| public override void VisitDestructorDeclaration(DestructorDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<DestructorDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => "<Destructor>", | |||||
| targetNode: n => n, | |||||
| visit: base.VisitDestructorDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits parameter list. | |||||
| /// </summary> | |||||
| /// <param name="node">The parameter list to visit.</param> | |||||
| 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<ParameterListSyntax>( | |||||
| 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); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a member. | |||||
| /// </summary> | |||||
| /// <typeparam name="T">The syntax node type to visit.</typeparam> | |||||
| /// <param name="node">The node to visit.</param> | |||||
| /// <param name="exctractName">Extract the node name.</param> | |||||
| /// <param name="typeParameterList">The type parameter list.</param> | |||||
| /// <param name="targetNode">Resolved the target node.</param> | |||||
| /// <param name="visit">Visit sub nodes.</param> | |||||
| private void Visit<T>(T node, Func<T, string> exctractName, TypeParameterListSyntax typeParameterList , Func<T, SyntaxNode> targetNode, Action<T> 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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")] | |||||
| @@ -0,0 +1,9 @@ | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <packages> | |||||
| <package id="Microsoft.CodeAnalysis.Analyzers" version="1.1.0" targetFramework="net45" /> | |||||
| <package id="Microsoft.CodeAnalysis.Common" version="1.3.2" targetFramework="net45" /> | |||||
| <package id="Microsoft.CodeAnalysis.CSharp" version="1.3.2" targetFramework="net45" /> | |||||
| <package id="System.Collections.Immutable" version="1.1.37" targetFramework="net45" /> | |||||
| <package id="System.IO.Abstractions" version="2.0.0.136" targetFramework="net45" /> | |||||
| <package id="System.Reflection.Metadata" version="1.2.0" targetFramework="net45" /> | |||||
| </packages> | |||||
| @@ -0,0 +1,68 @@ | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
| <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> | |||||
| <PropertyGroup> | |||||
| <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | |||||
| <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | |||||
| <ProjectGuid>{BC3E43EB-2263-49B4-883A-B720EDDF9298}</ProjectGuid> | |||||
| <OutputType>Library</OutputType> | |||||
| <AppDesignerFolder>Properties</AppDesignerFolder> | |||||
| <RootNamespace>Projbook.Extension.XmlExtractor</RootNamespace> | |||||
| <AssemblyName>Projbook.Extension.XmlExtractor</AssemblyName> | |||||
| <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> | |||||
| <FileAlignment>512</FileAlignment> | |||||
| <TargetFrameworkProfile /> | |||||
| </PropertyGroup> | |||||
| <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | |||||
| <DebugSymbols>true</DebugSymbols> | |||||
| <DebugType>full</DebugType> | |||||
| <Optimize>false</Optimize> | |||||
| <OutputPath>bin\Debug\</OutputPath> | |||||
| <DefineConstants>DEBUG;TRACE</DefineConstants> | |||||
| <ErrorReport>prompt</ErrorReport> | |||||
| <WarningLevel>4</WarningLevel> | |||||
| </PropertyGroup> | |||||
| <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | |||||
| <DebugType>pdbonly</DebugType> | |||||
| <Optimize>true</Optimize> | |||||
| <OutputPath>bin\Release\</OutputPath> | |||||
| <DefineConstants>TRACE</DefineConstants> | |||||
| <ErrorReport>prompt</ErrorReport> | |||||
| <WarningLevel>4</WarningLevel> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <Reference Include="System" /> | |||||
| <Reference Include="System.Core" /> | |||||
| <Reference Include="System.IO.Abstractions, Version=2.0.0.136, Culture=neutral, processorArchitecture=MSIL"> | |||||
| <HintPath>..\packages\System.IO.Abstractions.2.0.0.136\lib\net40\System.IO.Abstractions.dll</HintPath> | |||||
| <Private>True</Private> | |||||
| </Reference> | |||||
| <Reference Include="System.Xml.Linq" /> | |||||
| <Reference Include="System.Data.DataSetExtensions" /> | |||||
| <Reference Include="Microsoft.CSharp" /> | |||||
| <Reference Include="System.Data" /> | |||||
| <Reference Include="System.Net.Http" /> | |||||
| <Reference Include="System.Xml" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <Compile Include="Projbook\Extension\Xml\XmlSnippetExtractor.cs" /> | |||||
| <Compile Include="Properties\AssemblyInfo.cs" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\Projbook.Extension\Projbook.Extension.csproj"> | |||||
| <Project>{8338b756-0519-4d20-ba04-3a8f4839237a}</Project> | |||||
| <Name>Projbook.Extension</Name> | |||||
| </ProjectReference> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <None Include="packages.config" /> | |||||
| </ItemGroup> | |||||
| <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | |||||
| <!-- To modify your build process, add your task inside one of the targets below and uncomment it. | |||||
| Other similar extension points exist, see Microsoft.Common.targets. | |||||
| <Target Name="BeforeBuild"> | |||||
| </Target> | |||||
| <Target Name="AfterBuild"> | |||||
| </Target> | |||||
| --> | |||||
| </Project> | |||||
| @@ -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 | |||||
| { | |||||
| /// <summary> | |||||
| /// Extractor in charge of browsing source directories. load file content and extract requested member. | |||||
| /// </summary> | |||||
| [Syntax(name: "xml")] | |||||
| public class XmlSnippetExtractor : DefaultSnippetExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// The regex extracting the document namespaces | |||||
| /// </summary> | |||||
| private Regex regex = new Regex(@"xmlns:([^=]+)=""([^""]*)""", RegexOptions.Compiled); | |||||
| /// <summary> | |||||
| /// The lazy loaded xml document. | |||||
| /// </summary> | |||||
| private XmlDocument xmlDocument; | |||||
| /// <summary> | |||||
| /// The lazy loaded namespace manager. | |||||
| /// </summary> | |||||
| private XmlNamespaceManager xmlNamespaceManager; | |||||
| /// <summary> | |||||
| /// Extracts a snippet from a given rule pattern. | |||||
| /// </summary> | |||||
| /// <param name="fileSystemInfo">The file system info.</param> | |||||
| /// <param name="memberPattern">The member pattern to extract.</param> | |||||
| /// <returns>The extracted snippet.</returns> | |||||
| 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); | |||||
| } | |||||
| /// <summary> | |||||
| /// Builds a snippet from xml node. | |||||
| /// </summary> | |||||
| /// <param name="xmlNodeList">The xml node list.</param> | |||||
| /// <returns>The built snippet.</returns> | |||||
| 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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")] | |||||
| @@ -0,0 +1,5 @@ | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <packages> | |||||
| <package id="Ensure.That" version="5.0.0" targetFramework="net45" /> | |||||
| <package id="System.IO.Abstractions" version="2.0.0.136" targetFramework="net45" /> | |||||
| </packages> | |||||
| @@ -0,0 +1,53 @@ | |||||
| using System.Text; | |||||
| using System; | |||||
| using Projbook.Extension.Spi; | |||||
| using System.IO; | |||||
| namespace Projbook.Extension | |||||
| { | |||||
| /// <summary> | |||||
| /// Extractor in charge of browsing source directories. load file content and extract requested member. | |||||
| /// </summary> | |||||
| public class DefaultSnippetExtractor : ISnippetExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// File target type. | |||||
| /// </summary> | |||||
| public TargetType TargetType { get { return TargetType.File; } } | |||||
| /// <summary> | |||||
| /// Extracts a snippet. | |||||
| /// </summary> | |||||
| /// <param name="fullFilename">The full filename (with path) to load and to extract the snippet from.</param> | |||||
| /// <param name="pattern">The extraction pattern, never used for this implementation.</param> | |||||
| /// <returns> | |||||
| /// The extracted snippet. | |||||
| /// </returns> | |||||
| /// <exception cref="System.ArgumentNullException">fileSystemInfo</exception> | |||||
| public virtual string Extract(string fullFilename, string pattern) | |||||
| { | |||||
| if(string.IsNullOrEmpty(fullFilename)) | |||||
| { | |||||
| throw new ArgumentNullException(nameof(fullFilename)); | |||||
| } | |||||
| return this.LoadFile(fullFilename) ?? string.Empty; | |||||
| } | |||||
| /// <summary> | |||||
| /// Loads a file from the file name. | |||||
| /// </summary> | |||||
| /// <param name="fullFilename">The full filename.</param> | |||||
| /// <returns> | |||||
| /// The file's content. | |||||
| /// </returns> | |||||
| /// <exception cref="System.ArgumentNullException">fileInfo</exception> | |||||
| protected string LoadFile(string fullFilename) | |||||
| { | |||||
| if(string.IsNullOrEmpty(fullFilename)) | |||||
| { | |||||
| throw new ArgumentNullException(nameof(fullFilename)); | |||||
| } | |||||
| return File.ReadAllText(fullFilename, Encoding.UTF8); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,25 @@ | |||||
| namespace Projbook.Extension.Exception | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents a snippet extraction exception. | |||||
| /// </summary> | |||||
| public class SnippetExtractionException : System.Exception | |||||
| { | |||||
| /// <summary> | |||||
| /// The pattern the exception is about. | |||||
| /// </summary> | |||||
| public string Pattern { get; private set; } | |||||
| /// <summary> | |||||
| /// Initializes a new instance of <see cref="ProjbookEngine"/>. | |||||
| /// </summary> | |||||
| /// <param name="message">Initializes the required <see cref="Message"/>.</param> | |||||
| /// <param name="pattern">Initializes the required <see cref="Pattern"/>.</param> | |||||
| public SnippetExtractionException(string message, string pattern) | |||||
| : base(message) | |||||
| { | |||||
| // Initialize | |||||
| this.Pattern = pattern; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,23 @@ | |||||
| namespace Projbook.Extension.CSharpExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents the extraction mode. | |||||
| /// </summary> | |||||
| public enum CSharpExtractionMode | |||||
| { | |||||
| /// <summary> | |||||
| /// Full member: Do not process the snippet and print it as it. | |||||
| /// </summary> | |||||
| FullMember, | |||||
| /// <summary> | |||||
| /// Content only: Extract the code block and print this part only. | |||||
| /// </summary> | |||||
| ContentOnly, | |||||
| /// <summary> | |||||
| /// Block structure only: Remove the block content and print the code structure only. | |||||
| /// </summary> | |||||
| BlockStructureOnly | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,80 @@ | |||||
| using Projbook.Extension.Exception; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Text.RegularExpressions; | |||||
| namespace Projbook.Extension.CSharpExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents a matching rule for referencing a C# member. | |||||
| /// </summary> | |||||
| public class CSharpMatchingRule | |||||
| { | |||||
| /// <summary> | |||||
| /// The matching chunk to identify which member are the snippet targets. | |||||
| /// </summary> | |||||
| public string[] MatchingChunks { get; private set; } | |||||
| /// <summary> | |||||
| /// The snippet extraction mode. | |||||
| /// </summary> | |||||
| public CSharpExtractionMode ExtractionMode { get; private set; } | |||||
| /// <summary> | |||||
| /// 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 <seealso cref="TargetFile"/> | |||||
| /// * 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. | |||||
| /// </summary> | |||||
| private static Regex ruleRegex = new Regex(@"^([-=])?([^(]+)?\s*(\([^)]*\s*\))?\s*$", RegexOptions.Compiled); | |||||
| /// <summary> | |||||
| /// Parses the token | |||||
| /// </summary> | |||||
| /// <param name="pattern"></param> | |||||
| /// <returns></returns> | |||||
| 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<string> matchingChunks = new List<string>(); | |||||
| 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 | |||||
| }; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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 | |||||
| { | |||||
| /// <summary> | |||||
| /// Extractor in charge of browsing source directories. load file content and extract requested member. | |||||
| /// </summary> | |||||
| public class CSharpSnippetExtractor : DefaultSnippetExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| private CSharpSyntaxMatchingNode syntaxTrie; | |||||
| /// <summary> | |||||
| /// Extracts a snippet from a given rule pattern. | |||||
| /// </summary> | |||||
| /// <param name="fullFilename">The full filename (with path) to load and to extract the snippet from.</param> | |||||
| /// <param name="memberPattern">The member pattern to extract.</param> | |||||
| /// <returns>The extracted snippet.</returns> | |||||
| 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); | |||||
| } | |||||
| /// <summary> | |||||
| /// Builds a snippet from extracted syntax nodes. | |||||
| /// </summary> | |||||
| /// <param name="nodes">The exctracted nodes.</param> | |||||
| /// <param name="extractionMode">The extraction mode.</param> | |||||
| /// <returns>The built snippet.</returns> | |||||
| 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(); | |||||
| } | |||||
| /// <summary> | |||||
| /// Determines the content's block position depending on the node type. | |||||
| /// </summary> | |||||
| /// <param name="node">The node to extract the content position from.</param> | |||||
| /// <returns>The determined content position or 0 if not found.</returns> | |||||
| 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<AccessorListSyntax>().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<BlockSyntax>().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; | |||||
| } | |||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| /// <param name="stringBuilder">The string builder used as output.</param> | |||||
| /// <param name="lines">The lines to process.</param> | |||||
| /// <param name="extractionMode">The extraction mode.</param> | |||||
| /// <param name="contentPosition">The content position.</param> | |||||
| 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,207 @@ | |||||
| using System; | |||||
| using Microsoft.CodeAnalysis; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| namespace Projbook.Extension.CSharpExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents a syntax matching node. | |||||
| /// Thie node is used to build a Trie representing possible matching. | |||||
| /// Each node contians children and matching syntax nodes. | |||||
| /// </summary> | |||||
| public class CSharpSyntaxMatchingNode | |||||
| { | |||||
| /// <summary> | |||||
| /// The public Matching SyntaxNodes. | |||||
| /// </summary> | |||||
| 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(); | |||||
| } | |||||
| } | |||||
| /// <summary> | |||||
| /// The node's children. | |||||
| /// </summary> | |||||
| private Dictionary<string, CSharpSyntaxMatchingNode> children; | |||||
| /// <summary> | |||||
| /// The node's maching syntax node. | |||||
| /// </summary> | |||||
| private List<SyntaxNode> matchingSyntaxNodes; | |||||
| /// <summary> | |||||
| /// Finds a node from syntax chunk. | |||||
| /// </summary> | |||||
| /// <param name="chunks">The chunks to match.</param> | |||||
| /// <returns></returns> | |||||
| 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; | |||||
| } | |||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| /// <param name="name">The node name.</param> | |||||
| /// <returns>The node matching the requested name.</returns> | |||||
| 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<string, CSharpSyntaxMatchingNode>(); | |||||
| } | |||||
| // Assign and return the new node | |||||
| return this.children[name] = new CSharpSyntaxMatchingNode(); | |||||
| } | |||||
| /// <summary> | |||||
| /// Adds a syntax node as matching node. | |||||
| /// </summary> | |||||
| /// <param name="node"></param> | |||||
| 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<SyntaxNode>(); | |||||
| } | |||||
| // Add the node to the known matching node | |||||
| this.matchingSyntaxNodes.Add(node); | |||||
| } | |||||
| /// <summary> | |||||
| /// Copies to a given node. | |||||
| /// </summary> | |||||
| /// <param name="targetNode">The node wherer to copy.</param> | |||||
| /// <param name="name">The node name.</param> | |||||
| 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<SyntaxNode>(); | |||||
| } | |||||
| // 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| /// <summary> | |||||
| /// Overrides ToString to renger the internal Trie to a string. | |||||
| /// </summary> | |||||
| /// <returns>The rendered Trie as string.</returns> | |||||
| public override string ToString() | |||||
| { | |||||
| StringBuilder strinbBuilder = new StringBuilder(); | |||||
| this.Write(strinbBuilder, null, 0); | |||||
| return strinbBuilder.ToString(); | |||||
| } | |||||
| /// <summary> | |||||
| /// Writes a node to a string builder and recurse to the children. | |||||
| /// </summary> | |||||
| /// <param name="stringBuilder">The string builder used as output.</param> | |||||
| /// <param name="name">The node name.</param> | |||||
| /// <param name="level">The node level.</param> | |||||
| 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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 | |||||
| { | |||||
| /// <summary> | |||||
| /// Implements a syntax walker generating a Trie for pattern matching. | |||||
| /// </summary> | |||||
| public class CSharpSyntaxWalkerMatchingBuilder : CSharpSyntaxWalker | |||||
| { | |||||
| /// <summary> | |||||
| /// The current Trie root available from the outside. | |||||
| /// </summary> | |||||
| public CSharpSyntaxMatchingNode Root { get; private set; } | |||||
| /// <summary> | |||||
| /// The Trie root referencing the root without any reference change. | |||||
| /// </summary> | |||||
| private CSharpSyntaxMatchingNode internalInvariantRoot; | |||||
| /// <summary> | |||||
| /// Initializes a new instance of <see cref="CSharpSyntaxWalkerMatchingBuilder"/>. | |||||
| /// </summary> | |||||
| public CSharpSyntaxWalkerMatchingBuilder() | |||||
| { | |||||
| this.internalInvariantRoot = new CSharpSyntaxMatchingNode(); | |||||
| this.Root = this.internalInvariantRoot; | |||||
| } | |||||
| /// <summary> | |||||
| /// 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. | |||||
| /// </summary> | |||||
| /// <param name="node">The namespace declaration node to visit.</param> | |||||
| 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; | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a class declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The class declaration to visit.</param> | |||||
| public override void VisitClassDeclaration(ClassDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<ClassDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: node.TypeParameterList, | |||||
| exctractName: n => node.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitClassDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an interface declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The class declaration to visit.</param> | |||||
| public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<InterfaceDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: node.TypeParameterList, | |||||
| exctractName: n => node.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitInterfaceDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an enum declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The enum declaration to visit.</param> | |||||
| public override void VisitEnumDeclaration(EnumDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<EnumDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => node.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitEnumDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an enum member declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The enum member declaration to visit.</param> | |||||
| public override void VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<EnumMemberDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => node.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitEnumMemberDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a property declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The property declaration to visit.</param> | |||||
| public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<PropertyDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => n.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitPropertyDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a field declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The field declaration to visit.</param> | |||||
| public override void VisitFieldDeclaration(FieldDeclarationSyntax node) | |||||
| { | |||||
| // Visit each variable declaration | |||||
| foreach(VariableDeclaratorSyntax variableDeclarationSyntax in node.Declaration.Variables) | |||||
| { | |||||
| this.Visit<FieldDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => variableDeclarationSyntax.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitFieldDeclaration); | |||||
| } | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an indexter declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The indexter declaration to visit.</param> | |||||
| 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<IndexerDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => memberName, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitIndexerDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an event declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The event declaration to visit.</param> | |||||
| public override void VisitEventDeclaration(EventDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<EventDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => n.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitEventDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits an accessor declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The accessor declaration to visit.</param> | |||||
| public override void VisitAccessorDeclaration(AccessorDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<AccessorDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => n.Keyword.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitAccessorDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a method declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The method declaration to visit.</param> | |||||
| public override void VisitMethodDeclaration(MethodDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<MethodDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: node.TypeParameterList, | |||||
| exctractName: n => n.Identifier.ValueText, | |||||
| targetNode: n => n, | |||||
| visit: base.VisitMethodDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a constructor declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The constructor declaration to visit.</param> | |||||
| public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<ConstructorDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => "<Constructor>", | |||||
| targetNode: n => n, | |||||
| visit: base.VisitConstructorDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a destructor declaration. | |||||
| /// </summary> | |||||
| /// <param name="node">The destructor declaration to visit.</param> | |||||
| public override void VisitDestructorDeclaration(DestructorDeclarationSyntax node) | |||||
| { | |||||
| // Visit | |||||
| this.Visit<DestructorDeclarationSyntax>( | |||||
| node: node, | |||||
| typeParameterList: null, | |||||
| exctractName: n => "<Destructor>", | |||||
| targetNode: n => n, | |||||
| visit: base.VisitDestructorDeclaration); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits parameter list. | |||||
| /// </summary> | |||||
| /// <param name="node">The parameter list to visit.</param> | |||||
| 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<ParameterListSyntax>( | |||||
| 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); | |||||
| } | |||||
| /// <summary> | |||||
| /// Visits a member. | |||||
| /// </summary> | |||||
| /// <typeparam name="T">The syntax node type to visit.</typeparam> | |||||
| /// <param name="node">The node to visit.</param> | |||||
| /// <param name="exctractName">Extract the node name.</param> | |||||
| /// <param name="typeParameterList">The type parameter list.</param> | |||||
| /// <param name="targetNode">Resolved the target node.</param> | |||||
| /// <param name="visit">Visit sub nodes.</param> | |||||
| private void Visit<T>(T node, Func<T, string> exctractName, TypeParameterListSyntax typeParameterList , Func<T, SyntaxNode> targetNode, Action<T> 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,162 @@ | |||||
| using System; | |||||
| using Projbook.Extension.Exception; | |||||
| using System.Text; | |||||
| using System.Text.RegularExpressions; | |||||
| using System.Xml; | |||||
| namespace Projbook.Extension.XmlExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// Extractor in charge of browsing source directories. load file content and extract requested member. | |||||
| /// </summary> | |||||
| public class XmlSnippetExtractor : DefaultSnippetExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// The regex extracting the document namespaces | |||||
| /// </summary> | |||||
| private Regex regex = new Regex(@"xmlns:([^=]+)=""([^""]*)""", RegexOptions.Compiled); | |||||
| /// <summary> | |||||
| /// The lazy loaded xml document. | |||||
| /// </summary> | |||||
| private XmlDocument xmlDocument; | |||||
| /// <summary> | |||||
| /// The lazy loaded namespace manager. | |||||
| /// </summary> | |||||
| private XmlNamespaceManager xmlNamespaceManager; | |||||
| /// <summary> | |||||
| /// Extracts a snippet from a given rule pattern. | |||||
| /// </summary> | |||||
| /// <param name="fullFilename">The full filename (with path) to load and to extract the snippet from.</param> | |||||
| /// <param name="memberPattern">The member pattern to extract.</param> | |||||
| /// <returns> | |||||
| /// The extracted snippet. | |||||
| /// </returns> | |||||
| /// <exception cref="SnippetExtractionException"> | |||||
| /// Cannot parse xml file | |||||
| /// or | |||||
| /// Invalid extraction rule | |||||
| /// or | |||||
| /// Cannot find member | |||||
| /// </exception> | |||||
| 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); | |||||
| } | |||||
| /// <summary> | |||||
| /// Builds a snippet from xml node. | |||||
| /// </summary> | |||||
| /// <param name="xmlNodeList">The xml node list.</param> | |||||
| /// <returns>The built snippet.</returns> | |||||
| 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,90 @@ | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
| <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> | |||||
| <PropertyGroup> | |||||
| <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | |||||
| <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | |||||
| <ProjectGuid>{8338B756-0519-4D20-BA04-3A8F4839237A}</ProjectGuid> | |||||
| <OutputType>Library</OutputType> | |||||
| <AppDesignerFolder>Properties</AppDesignerFolder> | |||||
| <RootNamespace>Projbook.Extension</RootNamespace> | |||||
| <AssemblyName>Projbook.Extension</AssemblyName> | |||||
| <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> | |||||
| <FileAlignment>512</FileAlignment> | |||||
| <TargetFrameworkProfile /> | |||||
| </PropertyGroup> | |||||
| <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | |||||
| <DebugSymbols>true</DebugSymbols> | |||||
| <DebugType>full</DebugType> | |||||
| <Optimize>false</Optimize> | |||||
| <OutputPath>bin\Debug\</OutputPath> | |||||
| <DefineConstants>DEBUG;TRACE</DefineConstants> | |||||
| <ErrorReport>prompt</ErrorReport> | |||||
| <WarningLevel>4</WarningLevel> | |||||
| </PropertyGroup> | |||||
| <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | |||||
| <DebugType>pdbonly</DebugType> | |||||
| <Optimize>true</Optimize> | |||||
| <OutputPath>bin\Release\</OutputPath> | |||||
| <DefineConstants>TRACE</DefineConstants> | |||||
| <ErrorReport>prompt</ErrorReport> | |||||
| <WarningLevel>4</WarningLevel> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <Reference Include="Microsoft.CodeAnalysis, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |||||
| <HintPath>..\..\packages\Microsoft.CodeAnalysis.Common.1.3.2\lib\net45\Microsoft.CodeAnalysis.dll</HintPath> | |||||
| <Private>True</Private> | |||||
| </Reference> | |||||
| <Reference Include="Microsoft.CodeAnalysis.CSharp, Version=1.3.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |||||
| <HintPath>..\..\packages\Microsoft.CodeAnalysis.CSharp.1.3.2\lib\net45\Microsoft.CodeAnalysis.CSharp.dll</HintPath> | |||||
| <Private>True</Private> | |||||
| </Reference> | |||||
| <Reference Include="System" /> | |||||
| <Reference Include="System.Collections.Immutable, Version=1.1.37.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||||
| <HintPath>..\..\packages\System.Collections.Immutable.1.1.37\lib\dotnet\System.Collections.Immutable.dll</HintPath> | |||||
| <Private>True</Private> | |||||
| </Reference> | |||||
| <Reference Include="System.Core" /> | |||||
| <Reference Include="Microsoft.CSharp" /> | |||||
| <Reference Include="System.Reflection.Metadata, Version=1.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||||
| <HintPath>..\..\packages\System.Reflection.Metadata.1.2.0\lib\portable-net45+win8\System.Reflection.Metadata.dll</HintPath> | |||||
| <Private>True</Private> | |||||
| </Reference> | |||||
| <Reference Include="System.Threading.Thread, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||||
| <HintPath>..\..\packages\System.Threading.Thread.4.0.0\lib\net46\System.Threading.Thread.dll</HintPath> | |||||
| <Private>True</Private> | |||||
| </Reference> | |||||
| <Reference Include="System.Xml" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <Compile Include="Extractors\CSharp\CSharpExtractionMode.cs" /> | |||||
| <Compile Include="Extractors\CSharp\CSharpMatchingRule.cs" /> | |||||
| <Compile Include="Extractors\CSharp\CSharpSnippetExtractor.cs" /> | |||||
| <Compile Include="Extractors\CSharp\CSharpSyntaxMatchingNode.cs" /> | |||||
| <Compile Include="Extractors\CSharp\CSharpSyntaxWalkerMatchingBuilder.cs" /> | |||||
| <Compile Include="DefaultSnippetExtractor.cs" /> | |||||
| <Compile Include="Exception\SnippetExtractionException.cs" /> | |||||
| <Compile Include="Spi\ISnippetExtractor.cs" /> | |||||
| <Compile Include="Spi\TargetType.cs" /> | |||||
| <Compile Include="Properties\AssemblyInfo.cs" /> | |||||
| <Compile Include="Extractors\Xml\XmlSnippetExtractor.cs" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <None Include="app.config" /> | |||||
| <None Include="packages.config"> | |||||
| <SubType>Designer</SubType> | |||||
| </None> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <Analyzer Include="..\..\packages\Microsoft.CodeAnalysis.Analyzers.1.1.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.Analyzers.dll" /> | |||||
| <Analyzer Include="..\..\packages\Microsoft.CodeAnalysis.Analyzers.1.1.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.CSharp.Analyzers.dll" /> | |||||
| </ItemGroup> | |||||
| <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | |||||
| <!-- To modify your build process, add your task inside one of the targets below and uncomment it. | |||||
| Other similar extension points exist, see Microsoft.Common.targets. | |||||
| <Target Name="BeforeBuild"> | |||||
| </Target> | |||||
| <Target Name="AfterBuild"> | |||||
| </Target> | |||||
| --> | |||||
| </Project> | |||||
| @@ -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")] | |||||
| @@ -0,0 +1,24 @@ | |||||
| namespace Projbook.Extension.Spi | |||||
| { | |||||
| /// <summary> | |||||
| /// Defines interface for snippet extractor. | |||||
| /// </summary> | |||||
| public interface ISnippetExtractor | |||||
| { | |||||
| /// <summary> | |||||
| /// Defines the target type. | |||||
| /// </summary> | |||||
| TargetType TargetType { get; } | |||||
| /// <summary> | |||||
| /// Extracts a snippet. | |||||
| /// </summary> | |||||
| /// <param name="fullFilename">The full filename (with path) to load and to extract the snippet from.</param> | |||||
| /// <param name="pattern">The extraction pattern.</param> | |||||
| /// <returns> | |||||
| /// The extracted snippet as string. | |||||
| /// </returns> | |||||
| string Extract(string fullFilename, string pattern); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,23 @@ | |||||
| namespace Projbook.Extension.Spi | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents an extraction target. | |||||
| /// </summary> | |||||
| public enum TargetType | |||||
| { | |||||
| /// <summary> | |||||
| /// Free text target, used by plugins extracting from free value. | |||||
| /// </summary> | |||||
| FreeText, | |||||
| /// <summary> | |||||
| /// File target, used by plugins extracting from a file. | |||||
| /// </summary> | |||||
| File, | |||||
| /// <summary> | |||||
| /// Folder target, ised bu plugins extracting from a folder. | |||||
| /// </summary> | |||||
| Folder | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,15 @@ | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <configuration> | |||||
| <runtime> | |||||
| <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> | |||||
| <dependentAssembly> | |||||
| <assemblyIdentity name="System.Reflection.Metadata" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/> | |||||
| <bindingRedirect oldVersion="0.0.0.0-1.3.0.0" newVersion="1.3.0.0"/> | |||||
| </dependentAssembly> | |||||
| <dependentAssembly> | |||||
| <assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/> | |||||
| <bindingRedirect oldVersion="0.0.0.0-1.2.0.0" newVersion="1.2.0.0"/> | |||||
| </dependentAssembly> | |||||
| </assemblyBinding> | |||||
| </runtime> | |||||
| <startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1"/></startup></configuration> | |||||
| @@ -0,0 +1,16 @@ | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <packages> | |||||
| <package id="Microsoft.CodeAnalysis.Analyzers" version="1.1.0" targetFramework="net451" /> | |||||
| <package id="Microsoft.CodeAnalysis.Common" version="1.3.2" targetFramework="net451" /> | |||||
| <package id="Microsoft.CodeAnalysis.CSharp" version="1.3.2" targetFramework="net451" /> | |||||
| <package id="System.Collections" version="4.0.0" targetFramework="net451" /> | |||||
| <package id="System.Collections.Immutable" version="1.1.37" targetFramework="net451" /> | |||||
| <package id="System.Diagnostics.Debug" version="4.0.0" targetFramework="net451" /> | |||||
| <package id="System.Globalization" version="4.0.0" targetFramework="net451" /> | |||||
| <package id="System.Linq" version="4.0.0" targetFramework="net451" /> | |||||
| <package id="System.Reflection.Metadata" version="1.2.0" targetFramework="net451" /> | |||||
| <package id="System.Resources.ResourceManager" version="4.0.0" targetFramework="net451" /> | |||||
| <package id="System.Runtime" version="4.0.0" targetFramework="net451" /> | |||||
| <package id="System.Runtime.Extensions" version="4.0.0" targetFramework="net451" /> | |||||
| <package id="System.Threading" version="4.0.0" targetFramework="net451" /> | |||||
| </packages> | |||||