Skip to content

Commit 0b3df7b

Browse files
committed
- Implemented a #L10-20 syntax for files
- Implemented a #M-MethodName syntax for CSharp files to reference a specific method
1 parent 4962643 commit 0b3df7b

File tree

6 files changed

+138
-17
lines changed

6 files changed

+138
-17
lines changed

readme.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,32 @@ Will render:
230230
<sup><a href='#snippet-license.txt' title='Start of snippet'>anchor</a></sup>
231231
<!-- endSnippet -->
232232

233+
### Including a partial file
234+
235+
If no snippet is found matching the defined name. The target directory will be searched for a file matching that name.
236+
Using a syntax of #L10-20 you can only include lines 10 to 20 from that file. For example:
237+
238+
<!-- snippet: license.txt#L1-3 -->
239+
<a id='snippet-license.txt#L1-3'></a>
240+
```txt
241+
The MIT License (MIT)
242+
...
243+
```
244+
<sup><a href='#snippet-license.txt' title='Start of snippet'>anchor</a></sup>
245+
<!-- endSnippet -->
246+
247+
### Including a C# Method
248+
249+
If no snippet is found matching the defined name. The target directory will be searched for a file matching that name.
250+
Using a syntax of #M-MethodName you can reference a specific method. For example:
251+
252+
<!-- snippet: src\ConfigReader\LogBuilder.cs#M-GetHeader -->
253+
<a id='src\ConfigReader\LogBuilder.cs#M-GetHeader'></a>
254+
```cs
255+
...
256+
```
257+
<sup><a href='#src\ConfigReader\LogBuilder.cs' title='Start of snippet'>anchor</a></sup>
258+
<!-- endSnippet -->
233259

234260
### LinkFormat
235261

readme.source.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,32 @@ Will render:
223223
<sup><a href='#snippet-license.txt' title='Start of snippet'>anchor</a></sup>
224224
<!-- endSnippet -->
225225

226+
### Including a partial file
227+
228+
If no snippet is found matching the defined name. The target directory will be searched for a file matching that name.
229+
Using a syntax of #L10-20 you can only include lines 10 to 20 from that file. For example:
230+
231+
<!-- snippet: license.txt#L1-3 -->
232+
<a id='snippet-license.txt#L1-3'></a>
233+
```txt
234+
The MIT License (MIT)
235+
...
236+
```
237+
<sup><a href='#snippet-license.txt' title='Start of snippet'>anchor</a></sup>
238+
<!-- endSnippet -->
239+
240+
### Including a C# Method
241+
242+
If no snippet is found matching the defined name. The target directory will be searched for a file matching that name.
243+
Using a syntax of #M-MethodName you can reference a specific method. For example:
244+
245+
<!-- snippet: src\ConfigReader\LogBuilder.cs#M-GetHeader -->
246+
<a id='src\ConfigReader\LogBuilder.cs#M-GetHeader'></a>
247+
```cs
248+
...
249+
```
250+
<sup><a href='#src\ConfigReader\LogBuilder.cs' title='Start of snippet'>anchor</a></sup>
251+
<!-- endSnippet -->
226252

227253
### LinkFormat
228254

src/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<PackageVersion Include="Argon" Version="0.26.0" />
88
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
99
<PackageVersion Include="Microsoft.Build.Tasks.Core" Version="17.12.6" />
10+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
1011
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
1112
<PackageVersion Include="Polyfill" Version="7.12.0" />
1213
<PackageVersion Include="ProjectDefaults" Version="1.0.147" />

src/MarkdownSnippets/GlobalUsings.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
global using System.Diagnostics.CodeAnalysis;
1+
global using System.Diagnostics.CodeAnalysis;
22
global using System.Net;
33
global using System.Net.Http;
44
global using System.Text.RegularExpressions;
5-
global using MarkdownSnippets;
5+
global using MarkdownSnippets;
6+
7+
global using System.Text;
8+
global using Microsoft.CodeAnalysis;
9+
global using Microsoft.CodeAnalysis.CSharp;
10+
global using Microsoft.CodeAnalysis.CSharp.Syntax;

src/MarkdownSnippets/MarkdownSnippets.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
21
<Project Sdk="Microsoft.NET.Sdk">
32
<PropertyGroup>
43
<TargetFrameworks>netstandard2.0;net48;net8.0</TargetFrameworks>
54
</PropertyGroup>
65
<ItemGroup>
6+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
77
<PackageReference Include="ProjectDefaults" PrivateAssets="all" />
88
<PackageReference Include="Polyfill" PrivateAssets="all" />
99
<PackageReference Include="System.Memory" Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard' OR '$(TargetFrameworkIdentifier)' == '.NETFramework' OR '$(TargetFrameworkIdentifier)' == '.NETCOREAPP'" />

src/MarkdownSnippets/Processing/MarkdownProcessor.cs

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,18 @@ bool FilesToSnippets(
311311
return true;
312312
}
313313

314+
string? lineExpression = null;
315+
316+
if (key.Contains('#'))
317+
{
318+
var splitByHash = key.Split('#', StringSplitOptions.None);
319+
key = splitByHash[0];
320+
lineExpression = splitByHash[1];
321+
}
322+
314323
if (RelativeFile.Find(allFiles, targetDirectory, key, relativePath, linePath, out var path))
315324
{
316-
snippetsForKey = SnippetsForFile(key, path);
325+
snippetsForKey = SnippetsForFile(key, path, lineExpression);
317326
return true;
318327
}
319328

@@ -322,8 +331,7 @@ bool FilesToSnippets(
322331
}
323332

324333

325-
List<Snippet> SnippetsForFile(string key, string relativeToRoot) =>
326-
[FileToSnippet(key, relativeToRoot, null)];
334+
List<Snippet> SnippetsForFile(string key, string relativeToRoot, string? lines = null) => [FileToSnippet(key, relativeToRoot, null, lines)];
327335

328336
bool GetForHttp(string key, out IReadOnlyList<Snippet> snippetsForKey)
329337
{
@@ -338,22 +346,77 @@ bool GetForHttp(string key, out IReadOnlyList<Snippet> snippetsForKey)
338346
return true;
339347
}
340348

341-
Snippet FileToSnippet(string key, string file, string? path)
349+
Snippet FileToSnippet(string key, string file, string? path, string? linesExpression = null)
350+
{
351+
var language = Path.GetExtension(file)[1..];
352+
353+
if (linesExpression == null)
354+
{
355+
var (text, endLine) = ReadNonStartEndLines(file);
356+
return Snippet.Build(startLine: 1, endLine: endLine, value: text, key: key, language: language, path: path);
357+
}
358+
359+
if (linesExpression[0] == 'L')
360+
{
361+
return ParseSpecificLines(key, file, path, linesExpression, language);
362+
}
363+
364+
if (linesExpression[0] == 'M' && language == "cs")
365+
{
366+
return ParseCSharpMethod(key, file, path, linesExpression, language);
367+
}
368+
369+
throw new SnippetException($"Unable to parse expression '{linesExpression}'");
370+
}
371+
372+
static Snippet ParseCSharpMethod(string key, string file, string? path, string linesExpression, string language)
342373
{
343-
var (text, lineCount) = ReadNonStartEndLines(file);
374+
var methodName = linesExpression[2..];
375+
376+
var code = File.ReadAllText(file);
377+
var tree = CSharpSyntaxTree.ParseText(code);
378+
var root = tree.GetCompilationUnitRoot();
379+
380+
var method = root.DescendantNodes()
381+
.OfType<MethodDeclarationSyntax>()
382+
.FirstOrDefault(m => m.Identifier.Text == methodName);
383+
384+
if (method != null)
385+
{
386+
var text = method.ToFullString();
387+
var startLine = method.Span.Start;
388+
var endLine = method.Span.End;
389+
390+
return Snippet.Build(startLine: startLine, endLine: endLine, value: text, key: key, language: language, path: path);
391+
}
392+
393+
throw new SnippetException(
394+
$"""
395+
Failed to find method {methodName} in file {file} configuration.
396+
Content:
397+
{code}
398+
""");
399+
}
400+
401+
static Snippet ParseSpecificLines(string key, string file, string? path, string linesExpression, string language)
402+
{
403+
var text = string.Empty;
404+
405+
var expressionSplit = linesExpression.Split('-');
406+
var startLine = int.Parse(expressionSplit[0][1..]);
407+
var endLine = int.Parse(expressionSplit[1]);
408+
409+
var fileLines = File.ReadAllLines(file);
410+
411+
var selectedText = new StringBuilder();
344412

345-
if (lineCount == 0)
413+
for (var index = startLine; index < endLine; index++)
346414
{
347-
lineCount++;
415+
selectedText.AppendLine(fileLines[index]);
416+
text = selectedText.ToString();
348417
}
349418

350-
return Snippet.Build(
351-
startLine: 1,
352-
endLine: lineCount,
353-
value: text,
354-
key: key,
355-
language: Path.GetExtension(file)[1..],
356-
path: path);
419+
return Snippet.Build(startLine: startLine, endLine: endLine, value: text, key: key, language: language, path: path);
357420
}
358421

359422
(string text, int lineCount) ReadNonStartEndLines(string file)

0 commit comments

Comments
 (0)