diff --git a/readme.md b/readme.md index 8be83bb1..f5b85fae 100644 --- a/readme.md +++ b/readme.md @@ -219,11 +219,25 @@ Files are downloaded to `%temp%MarkdownSnippets` with a maximum of 100 files kep Will render: - + ```txt Some code ``` - anchor + anchor + + +You can optionally provide a second URL that will be used for the source link. This is useful when the raw content URL is different from the view URL. For example: + +`web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet` + +Will render: + + + + ```txt + Some code + ``` + anchor ### Including a full file @@ -347,7 +361,7 @@ switch (linkFormat) throw new($"Unknown LinkFormat: {linkFormat}"); } ``` -snippet source | anchor +snippet source | anchor diff --git a/readme.source.md b/readme.source.md index 5c290083..8023a1e7 100644 --- a/readme.source.md +++ b/readme.source.md @@ -212,11 +212,25 @@ Files are downloaded to `%temp%MarkdownSnippets` with a maximum of 100 files kep Will render: - + ```txt Some code ``` - anchor + anchor + + +You can optionally provide a second URL that will be used for the source link. This is useful when the raw content URL is different from the view URL. For example: + +`web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet` + +Will render: + + + + ```txt + Some code + ``` + anchor ### Including a full file diff --git a/src/MarkdownSnippets/Processing/MarkdownProcessor.cs b/src/MarkdownSnippets/Processing/MarkdownProcessor.cs index 318aac61..34c5c44d 100644 --- a/src/MarkdownSnippets/Processing/MarkdownProcessor.cs +++ b/src/MarkdownSnippets/Processing/MarkdownProcessor.cs @@ -190,11 +190,11 @@ void AppendSnippet(string key1) line.Current = builder.ToString(); } - void AppendWebSnippet(string url, string snippetKey) + void AppendWebSnippet(string url, string snippetKey, string? viewUrl = null) { builder.Clear(); var indentedAppendLine = CreateIndentedAppendLine(line.LeadingWhitespace); - ProcessWebSnippetLine(indentedAppendLine, missingSnippets, usedSnippets, url, snippetKey, line); + ProcessWebSnippetLine(indentedAppendLine, missingSnippets, usedSnippets, url, snippetKey, viewUrl, line); builder.TrimEnd(); line.Current = builder.ToString(); } @@ -205,9 +205,9 @@ void AppendWebSnippet(string url, string snippetKey) continue; } - if (SnippetKey.ExtractWebSnippet(line, out var url, out var snippetKey)) + if (SnippetKey.ExtractWebSnippet(line, out var url, out var snippetKey, out var viewUrl)) { - AppendWebSnippet(url, snippetKey); + AppendWebSnippet(url, snippetKey, viewUrl); continue; } @@ -230,6 +230,20 @@ void AppendWebSnippet(string url, string snippetKey) continue; } + if (SnippetKey.ExtractStartCommentWebSnippet(line, out url, out snippetKey, out viewUrl)) + { + AppendWebSnippet(url, snippetKey, viewUrl); + + index++; + + lines.RemoveUntil( + index, + "", + relativePath, + line); + continue; + } + if (line.Current.TrimStart() == "") { tocLine = line; @@ -298,38 +312,65 @@ void ProcessSnippetLine(Action appendLine, List missings appendLine(""); } - void ProcessWebSnippetLine(Action appendLine, List missings, List used, string url, string snippetKey, Line line) + void ProcessWebSnippetLine(Action appendLine, List missings, List used, string url, string snippetKey, string? viewUrl, Line line) { - appendLine($""); + var commentText = viewUrl == null + ? $"" + : $""; + appendLine(commentText); // Download file content - var (success, content) = Downloader.DownloadContent(url).GetAwaiter().GetResult(); - if (!success || string.IsNullOrWhiteSpace(content)) + try { - var missing = new MissingSnippet($"{url}#{snippetKey}", line.LineNumber, line.Path); - missings.Add(missing); - appendLine("```"); - appendLine($"** Could not fetch or parse web-snippet '{url}#{snippetKey}' **"); - appendLine("```"); + var (success, content) = Downloader.DownloadContent(url).GetAwaiter().GetResult(); + if (!success || string.IsNullOrWhiteSpace(content)) + { + var missing = new MissingSnippet($"{url}#{snippetKey}", line.LineNumber, line.Path); + missings.Add(missing); + appendLine("```"); + appendLine($"** Could not fetch or parse web-snippet '{url}#{snippetKey}' **"); + appendLine("```"); + appendLine(""); + return; + } + // Extract snippets from content + using var reader = new StringReader(content); + var snippets = FileSnippetExtractor.Read(reader, url); + var found = snippets.FirstOrDefault(_ => _.Key == snippetKey); + if (found == null) + { + var missing = new MissingSnippet($"{url}#{snippetKey}", line.LineNumber, line.Path); + missings.Add(missing); + appendLine("```"); + appendLine($"** Could not find snippet '{snippetKey}' in '{url}' **"); + appendLine("```"); + appendLine(""); + return; + } + // Create new snippet with viewUrl if provided + var snippetToAppend = viewUrl == null + ? found + : Snippet.Build( + language: found.Language, + startLine: found.StartLine, + endLine: found.EndLine, + value: found.Value, + key: found.Key, + path: found.Path, + expressiveCode: found.ExpressiveCode, + viewUrl: viewUrl); + appendSnippets(snippetKey, [snippetToAppend], appendLine); appendLine(""); - return; + used.Add(snippetToAppend); } - // Extract snippets from content - using var reader = new StringReader(content); - var snippets = FileSnippetExtractor.Read(reader, url); - var found = snippets.FirstOrDefault(_ => _.Key == snippetKey); - if (found == null) + catch { var missing = new MissingSnippet($"{url}#{snippetKey}", line.LineNumber, line.Path); missings.Add(missing); appendLine("```"); - appendLine($"** Could not find snippet '{snippetKey}' in '{url}' **"); + appendLine($"** Could not fetch or parse web-snippet '{url}#{snippetKey}' **"); appendLine("```"); appendLine(""); - return; } - appendSnippets(snippetKey, [found], appendLine); - appendLine(""); - used.Add(found); } bool TryGetSnippets( diff --git a/src/MarkdownSnippets/Processing/SnippetKey.cs b/src/MarkdownSnippets/Processing/SnippetKey.cs index d067f76e..252b4ae1 100644 --- a/src/MarkdownSnippets/Processing/SnippetKey.cs +++ b/src/MarkdownSnippets/Processing/SnippetKey.cs @@ -16,6 +16,51 @@ public static bool ExtractStartCommentSnippet(Line line, [NotNullWhen(true)] out return true; } + public static bool ExtractStartCommentWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey) => + ExtractStartCommentWebSnippet(line, out url, out snippetKey, out _); + + public static bool ExtractStartCommentWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey, out string? viewUrl) + { + var lineCurrent = line.Current.TrimStart(); + if (!IsStartCommentWebSnippetLine(lineCurrent)) + { + url = null; + snippetKey = null; + viewUrl = null; + return false; + } + + var substring = lineCurrent[18..]; // after ""); + var value = substring[..indexOf].Trim(); + + // Check for optional second URL separated by whitespace + var parts = value.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries); + string firstPart; + if (parts.Length >= 2) + { + firstPart = parts[0]; + viewUrl = parts[1]; + } + else + { + firstPart = value; + viewUrl = null; + } + + var hashIndex = firstPart.LastIndexOf('#'); + if (hashIndex < 0 || hashIndex == firstPart.Length - 1) + { + url = null; + snippetKey = null; + viewUrl = null; + return false; + } + url = firstPart[..hashIndex]; + snippetKey = firstPart[(hashIndex + 1)..]; + return true; + } + public static bool ExtractSnippet(Line line, [NotNullWhen(true)] out string? key) { var lineCurrent = line.Current.TrimStart(); @@ -35,25 +80,45 @@ public static bool ExtractSnippet(Line line, [NotNullWhen(true)] out string? key return true; } - public static bool ExtractWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey) + public static bool ExtractWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey) => + ExtractWebSnippet(line, out url, out snippetKey, out _); + + public static bool ExtractWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey, out string? viewUrl) { var lineCurrent = line.Current.TrimStart(); if (!IsWebSnippetLine(lineCurrent)) { url = null; snippetKey = null; + viewUrl = null; return false; } - var value = lineCurrent[12..].Trim(); // after 'web-snippet:', fixed from 11 to 12 - var hashIndex = value.LastIndexOf('#'); - if (hashIndex < 0 || hashIndex == value.Length - 1) + var value = lineCurrent[12..].Trim(); // after 'web-snippet:' + + // Check for optional second URL separated by whitespace + var parts = value.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries); + string firstPart; + if (parts.Length >= 2) + { + firstPart = parts[0]; + viewUrl = parts[1]; + } + else + { + firstPart = value; + viewUrl = null; + } + + var hashIndex = firstPart.LastIndexOf('#'); + if (hashIndex < 0 || hashIndex == firstPart.Length - 1) { url = null; snippetKey = null; + viewUrl = null; return false; } - url = value[..hashIndex]; - snippetKey = value[(hashIndex + 1)..]; + url = firstPart[..hashIndex]; + snippetKey = firstPart[(hashIndex + 1)..]; return true; } @@ -65,4 +130,7 @@ public static bool IsStartCommentSnippetLine(string line) => public static bool IsWebSnippetLine(string line) => line.StartsWith("web-snippet:", StringComparison.OrdinalIgnoreCase); + + public static bool IsStartCommentWebSnippetLine(string line) => + line.StartsWith(" +```txt +Some code +``` + + +after +} \ No newline at end of file diff --git a/src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithCommentWebSnippetWithViewUrl.verified.txt b/src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithCommentWebSnippetWithViewUrl.verified.txt new file mode 100644 index 00000000..a533630c --- /dev/null +++ b/src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithCommentWebSnippetWithViewUrl.verified.txt @@ -0,0 +1,22 @@ +{ + UsedSnippets: [ + { + Key: snipPet, + Language: txt, + Value: Some code, + Error: , + FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt(1-3), + IsInError: false + } + ], + result: +before + + +```txt +Some code +``` + + +after +} diff --git a/src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithInlineWebSnippetWithViewUrl.verified.txt b/src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithInlineWebSnippetWithViewUrl.verified.txt new file mode 100644 index 00000000..a533630c --- /dev/null +++ b/src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithInlineWebSnippetWithViewUrl.verified.txt @@ -0,0 +1,22 @@ +{ + UsedSnippets: [ + { + Key: snipPet, + Language: txt, + Value: Some code, + Error: , + FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt(1-3), + IsInError: false + } + ], + result: +before + + +```txt +Some code +``` + + +after +} diff --git a/src/Tests/MarkdownProcessor/MarkdownProcessorTests.cs b/src/Tests/MarkdownProcessor/MarkdownProcessorTests.cs index 25af7d80..07c6f276 100644 --- a/src/Tests/MarkdownProcessor/MarkdownProcessorTests.cs +++ b/src/Tests/MarkdownProcessor/MarkdownProcessorTests.cs @@ -792,6 +792,68 @@ public Task WithIndentedMultiLineSnippet() ]); } + [Fact] + public Task WithCommentWebSnippetUpdate() + { + var content = """ + + before + + + OLD CONTENT + THAT SHOULD BE + REPLACED + + + after + + """; + + return SnippetVerifier.Verify( + DocumentConvention.InPlaceOverwrite, + content); + } + + [Fact] + public Task WithCommentWebSnippetWithViewUrl() + { + var content = """ + + before + + + OLD CONTENT + THAT SHOULD BE + REPLACED + + + after + + """; + + return SnippetVerifier.Verify( + DocumentConvention.InPlaceOverwrite, + content); + } + + [Fact] + public Task WithInlineWebSnippetWithViewUrl() + { + var content = """ + + before + + web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt + + after + + """; + + return SnippetVerifier.Verify( + DocumentConvention.SourceTransform, + content); + } + static Snippet SnippetBuild(string language, string key) => Snippet.Build( language: language, diff --git a/src/Tests/SnippetMarkdownHandlingTests.AppendWebSnippet.verified.txt b/src/Tests/SnippetMarkdownHandlingTests.AppendWebSnippet.verified.txt new file mode 100644 index 00000000..e264ecab --- /dev/null +++ b/src/Tests/SnippetMarkdownHandlingTests.AppendWebSnippet.verified.txt @@ -0,0 +1,5 @@ + +```cs +theValue +``` +anchor diff --git a/src/Tests/SnippetMarkdownHandlingTests.AppendWebSnippetWithViewUrl.verified.txt b/src/Tests/SnippetMarkdownHandlingTests.AppendWebSnippetWithViewUrl.verified.txt new file mode 100644 index 00000000..bfb85779 --- /dev/null +++ b/src/Tests/SnippetMarkdownHandlingTests.AppendWebSnippetWithViewUrl.verified.txt @@ -0,0 +1,5 @@ + +```cs +theValue +``` +anchor diff --git a/src/Tests/SnippetMarkdownHandlingTests.cs b/src/Tests/SnippetMarkdownHandlingTests.cs index 1b080f8f..259015c7 100644 --- a/src/Tests/SnippetMarkdownHandlingTests.cs +++ b/src/Tests/SnippetMarkdownHandlingTests.cs @@ -70,6 +70,49 @@ public Task AppendHashed() return Verify(builder.ToString()); } + [Fact] + public Task AppendWebSnippet() + { + var builder = new StringBuilder(); + var webSnippet = Snippet.Build( + startLine: 1, + endLine: 2, + value: "theValue", + key: "mysnippet", + language: "cs", + path: "http://example.com/file.cs", + expressiveCode: null); + var markdownHandling = new SnippetMarkdownHandling(Environment.CurrentDirectory, LinkFormat.GitHub, false); + using (var writer = new StringWriter(builder)) + { + markdownHandling.Append("key1", new List { webSnippet }, writer.WriteLine); + } + + return Verify(builder.ToString()); + } + + [Fact] + public Task AppendWebSnippetWithViewUrl() + { + var builder = new StringBuilder(); + var webSnippet = Snippet.Build( + startLine: 5, + endLine: 10, + value: "theValue", + key: "mysnippet", + language: "cs", + path: "http://example.com/raw/file.cs", + expressiveCode: null, + viewUrl: "https://github.com/user/repo/blob/main/file.cs"); + var markdownHandling = new SnippetMarkdownHandling(Environment.CurrentDirectory, LinkFormat.GitHub, false); + using (var writer = new StringWriter(builder)) + { + markdownHandling.Append("key1", new List { webSnippet }, writer.WriteLine); + } + + return Verify(builder.ToString()); + } + static List Snippets() => [Snippet.Build(1, 2, "theValue", "thekey", "thelanguage", Environment.CurrentDirectory, expressiveCode: null)]; } \ No newline at end of file