diff --git a/readme.md b/readme.md index b391cf15..437931b8 100644 --- a/readme.md +++ b/readme.md @@ -230,6 +230,32 @@ Will render: anchor +### Including a partial file + +If no snippet is found matching the defined name. The target directory will be searched for a file matching that name. +Using a syntax of #L10-20 you can only include lines 10 to 20 from that file. For example: + + + + ```txt + The MIT License (MIT) + ... + ``` + anchor + + +### Including a C# Method + +If no snippet is found matching the defined name. The target directory will be searched for a file matching that name. +Using a syntax of #M-MethodName you can reference a specific method. For example: + + + + ```cs + ... + ``` + anchor + ### LinkFormat diff --git a/readme.source.md b/readme.source.md index 03d14000..aba32738 100644 --- a/readme.source.md +++ b/readme.source.md @@ -223,6 +223,32 @@ Will render: anchor +### Including a partial file + +If no snippet is found matching the defined name. The target directory will be searched for a file matching that name. +Using a syntax of #L10-20 you can only include lines 10 to 20 from that file. For example: + + + + ```txt + The MIT License (MIT) + ... + ``` + anchor + + +### Including a C# Method + +If no snippet is found matching the defined name. The target directory will be searched for a file matching that name. +Using a syntax of #M-MethodName you can reference a specific method. For example: + + + + ```cs + ... + ``` + anchor + ### LinkFormat diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 394bb0ed..ae9fad6c 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -7,6 +7,7 @@ + diff --git a/src/MarkdownSnippets/GlobalUsings.cs b/src/MarkdownSnippets/GlobalUsings.cs index 42f8fa5e..eabc4967 100644 --- a/src/MarkdownSnippets/GlobalUsings.cs +++ b/src/MarkdownSnippets/GlobalUsings.cs @@ -1,5 +1,10 @@ -global using System.Diagnostics.CodeAnalysis; +global using System.Diagnostics.CodeAnalysis; global using System.Net; global using System.Net.Http; global using System.Text.RegularExpressions; -global using MarkdownSnippets; \ No newline at end of file +global using MarkdownSnippets; + +global using System.Text; +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp; +global using Microsoft.CodeAnalysis.CSharp.Syntax; \ No newline at end of file diff --git a/src/MarkdownSnippets/MarkdownSnippets.csproj b/src/MarkdownSnippets/MarkdownSnippets.csproj index c009ddf8..e46c4de7 100644 --- a/src/MarkdownSnippets/MarkdownSnippets.csproj +++ b/src/MarkdownSnippets/MarkdownSnippets.csproj @@ -1,9 +1,9 @@ - netstandard2.0;net48;net8.0 + diff --git a/src/MarkdownSnippets/Processing/MarkdownProcessor.cs b/src/MarkdownSnippets/Processing/MarkdownProcessor.cs index 01c606ca..e449da96 100644 --- a/src/MarkdownSnippets/Processing/MarkdownProcessor.cs +++ b/src/MarkdownSnippets/Processing/MarkdownProcessor.cs @@ -311,9 +311,18 @@ bool FilesToSnippets( return true; } + string? lineExpression = null; + + if (key.Contains('#')) + { + var splitByHash = key.Split('#', StringSplitOptions.None); + key = splitByHash[0]; + lineExpression = splitByHash[1]; + } + if (RelativeFile.Find(allFiles, targetDirectory, key, relativePath, linePath, out var path)) { - snippetsForKey = SnippetsForFile(key, path); + snippetsForKey = SnippetsForFile(key, path, lineExpression); return true; } @@ -322,8 +331,7 @@ bool FilesToSnippets( } - List SnippetsForFile(string key, string relativeToRoot) => - [FileToSnippet(key, relativeToRoot, null)]; + List SnippetsForFile(string key, string relativeToRoot, string? lines = null) => [FileToSnippet(key, relativeToRoot, null, lines)]; bool GetForHttp(string key, out IReadOnlyList snippetsForKey) { @@ -338,22 +346,77 @@ bool GetForHttp(string key, out IReadOnlyList snippetsForKey) return true; } - Snippet FileToSnippet(string key, string file, string? path) + Snippet FileToSnippet(string key, string file, string? path, string? linesExpression = null) + { + var language = Path.GetExtension(file)[1..]; + + if (linesExpression == null) + { + var (text, endLine) = ReadNonStartEndLines(file); + return Snippet.Build(startLine: 1, endLine: endLine, value: text, key: key, language: language, path: path); + } + + if (linesExpression[0] == 'L') + { + return ParseSpecificLines(key, file, path, linesExpression, language); + } + + if (linesExpression[0] == 'M' && language == "cs") + { + return ParseCSharpMethod(key, file, path, linesExpression, language); + } + + throw new SnippetException($"Unable to parse expression '{linesExpression}'"); + } + + static Snippet ParseCSharpMethod(string key, string file, string? path, string linesExpression, string language) { - var (text, lineCount) = ReadNonStartEndLines(file); + var methodName = linesExpression[2..]; + + var code = File.ReadAllText(file); + var tree = CSharpSyntaxTree.ParseText(code); + var root = tree.GetCompilationUnitRoot(); + + var method = root.DescendantNodes() + .OfType() + .FirstOrDefault(m => m.Identifier.Text == methodName); + + if (method != null) + { + var text = method.ToFullString(); + var startLine = method.Span.Start; + var endLine = method.Span.End; + + return Snippet.Build(startLine: startLine, endLine: endLine, value: text, key: key, language: language, path: path); + } + + throw new SnippetException( + $""" + Failed to find method {methodName} in file {file} configuration. + Content: + {code} + """); + } + + static Snippet ParseSpecificLines(string key, string file, string? path, string linesExpression, string language) + { + var text = string.Empty; + + var expressionSplit = linesExpression.Split('-'); + var startLine = int.Parse(expressionSplit[0][1..]); + var endLine = int.Parse(expressionSplit[1]); + + var fileLines = File.ReadAllLines(file); + + var selectedText = new StringBuilder(); - if (lineCount == 0) + for (var index = startLine; index < endLine; index++) { - lineCount++; + selectedText.AppendLine(fileLines[index]); + text = selectedText.ToString(); } - return Snippet.Build( - startLine: 1, - endLine: lineCount, - value: text, - key: key, - language: Path.GetExtension(file)[1..], - path: path); + return Snippet.Build(startLine: startLine, endLine: endLine, value: text, key: key, language: language, path: path); } (string text, int lineCount) ReadNonStartEndLines(string file)