Skip to content

Commit 4d82319

Browse files
committed
web-snippet can optionally take second URL
- If this exists, then it will be used for the a href link and line numbers will be passed (eg. suitable for GitHub content)
1 parent 0bae97d commit 4d82319

11 files changed

+260
-26
lines changed

readme.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,20 @@ Will render:
226226
<sup><a href='https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet' title='Snippet source file'>anchor</a></sup>
227227
<!-- endSnippet -->
228228

229+
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:
230+
231+
`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`
232+
233+
Will render:
234+
235+
<!-- 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 -->
236+
<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt%23snipPet'></a>
237+
```txt
238+
Some code
239+
```
240+
<sup><a href='https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet#L1-L3' title='Snippet source file'>anchor</a></sup>
241+
<!-- endSnippet -->
242+
229243
### Including a full file
230244

231245
If no snippet is found matching the defined name. The target directory will be searched for a file matching that name. For example:
@@ -347,7 +361,7 @@ switch (linkFormat)
347361
throw new($"Unknown LinkFormat: {linkFormat}");
348362
}
349363
```
350-
<sup><a href='/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs#L121-L142' title='Snippet source file'>snippet source</a> | <a href='#snippet-BuildLink' title='Start of snippet'>anchor</a></sup>
364+
<sup><a href='/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs#L125-L146' title='Snippet source file'>snippet source</a> | <a href='#snippet-BuildLink' title='Start of snippet'>anchor</a></sup>
351365
<!-- endSnippet -->
352366

353367

readme.source.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,25 @@ Files are downloaded to `%temp%MarkdownSnippets` with a maximum of 100 files kep
212212
Will render:
213213

214214
<!-- web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet -->
215-
<a id='snippet-snipPet'></a>
215+
<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt%23snipPet'></a>
216216
```txt
217217
Some code
218218
```
219-
<sup><a href='#snippet-snipPet' title='Start of snippet'>anchor</a></sup>
219+
<sup><a href='https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet' title='Snippet source file'>anchor</a></sup>
220+
<!-- endSnippet -->
221+
222+
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:
223+
224+
`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`
225+
226+
Will render:
227+
228+
<!-- 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 -->
229+
<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt%23snipPet'></a>
230+
```txt
231+
Some code
232+
```
233+
<sup><a href='https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet#L1-L3' title='Snippet source file'>anchor</a></sup>
220234
<!-- endSnippet -->
221235

222236
### Including a full file

src/MarkdownSnippets/Processing/MarkdownProcessor.cs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,11 @@ void AppendSnippet(string key1)
190190
line.Current = builder.ToString();
191191
}
192192

193-
void AppendWebSnippet(string url, string snippetKey)
193+
void AppendWebSnippet(string url, string snippetKey, string? viewUrl = null)
194194
{
195195
builder.Clear();
196196
var indentedAppendLine = CreateIndentedAppendLine(line.LeadingWhitespace);
197-
ProcessWebSnippetLine(indentedAppendLine, missingSnippets, usedSnippets, url, snippetKey, line);
197+
ProcessWebSnippetLine(indentedAppendLine, missingSnippets, usedSnippets, url, snippetKey, viewUrl, line);
198198
builder.TrimEnd();
199199
line.Current = builder.ToString();
200200
}
@@ -205,9 +205,9 @@ void AppendWebSnippet(string url, string snippetKey)
205205
continue;
206206
}
207207

208-
if (SnippetKey.ExtractWebSnippet(line, out var url, out var snippetKey))
208+
if (SnippetKey.ExtractWebSnippet(line, out var url, out var snippetKey, out var viewUrl))
209209
{
210-
AppendWebSnippet(url, snippetKey);
210+
AppendWebSnippet(url, snippetKey, viewUrl);
211211
continue;
212212
}
213213

@@ -230,9 +230,9 @@ void AppendWebSnippet(string url, string snippetKey)
230230
continue;
231231
}
232232

233-
if (SnippetKey.ExtractStartCommentWebSnippet(line, out url, out snippetKey))
233+
if (SnippetKey.ExtractStartCommentWebSnippet(line, out url, out snippetKey, out viewUrl))
234234
{
235-
AppendWebSnippet(url, snippetKey);
235+
AppendWebSnippet(url, snippetKey, viewUrl);
236236

237237
index++;
238238

@@ -312,9 +312,12 @@ void ProcessSnippetLine(Action<string> appendLine, List<MissingSnippet> missings
312312
appendLine("<!-- endSnippet -->");
313313
}
314314

315-
void ProcessWebSnippetLine(Action<string> appendLine, List<MissingSnippet> missings, List<Snippet> used, string url, string snippetKey, Line line)
315+
void ProcessWebSnippetLine(Action<string> appendLine, List<MissingSnippet> missings, List<Snippet> used, string url, string snippetKey, string? viewUrl, Line line)
316316
{
317-
appendLine($"<!-- web-snippet: {url}#{snippetKey} -->");
317+
var commentText = viewUrl == null
318+
? $"<!-- web-snippet: {url}#{snippetKey} -->"
319+
: $"<!-- web-snippet: {url}#{snippetKey} {viewUrl} -->";
320+
appendLine(commentText);
318321
// Download file content
319322
try
320323
{
@@ -343,9 +346,21 @@ void ProcessWebSnippetLine(Action<string> appendLine, List<MissingSnippet> missi
343346
appendLine("<!-- endSnippet -->");
344347
return;
345348
}
346-
appendSnippets(snippetKey, [found], appendLine);
349+
// Create new snippet with viewUrl if provided
350+
var snippetToAppend = viewUrl == null
351+
? found
352+
: Snippet.Build(
353+
language: found.Language,
354+
startLine: found.StartLine,
355+
endLine: found.EndLine,
356+
value: found.Value,
357+
key: found.Key,
358+
path: found.Path,
359+
expressiveCode: found.ExpressiveCode,
360+
viewUrl: viewUrl);
361+
appendSnippets(snippetKey, [snippetToAppend], appendLine);
347362
appendLine("<!-- endSnippet -->");
348-
used.Add(found);
363+
used.Add(snippetToAppend);
349364
}
350365
catch
351366
{

src/MarkdownSnippets/Processing/SnippetKey.cs

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,48 @@ public static bool ExtractStartCommentSnippet(Line line, [NotNullWhen(true)] out
1616
return true;
1717
}
1818

19-
public static bool ExtractStartCommentWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey)
19+
public static bool ExtractStartCommentWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey) =>
20+
ExtractStartCommentWebSnippet(line, out url, out snippetKey, out _);
21+
22+
public static bool ExtractStartCommentWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey, out string? viewUrl)
2023
{
2124
var lineCurrent = line.Current.TrimStart();
2225
if (!IsStartCommentWebSnippetLine(lineCurrent))
2326
{
2427
url = null;
2528
snippetKey = null;
29+
viewUrl = null;
2630
return false;
2731
}
2832

2933
var substring = lineCurrent[18..]; // after "<!-- web-snippet: "
3034
var indexOf = substring.IndexOf("-->");
3135
var value = substring[..indexOf].Trim();
3236

33-
var hashIndex = value.LastIndexOf('#');
34-
if (hashIndex < 0 || hashIndex == value.Length - 1)
37+
// Check for optional second URL separated by whitespace
38+
var parts = value.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
39+
string firstPart;
40+
if (parts.Length >= 2)
41+
{
42+
firstPart = parts[0];
43+
viewUrl = parts[1];
44+
}
45+
else
46+
{
47+
firstPart = value;
48+
viewUrl = null;
49+
}
50+
51+
var hashIndex = firstPart.LastIndexOf('#');
52+
if (hashIndex < 0 || hashIndex == firstPart.Length - 1)
3553
{
3654
url = null;
3755
snippetKey = null;
56+
viewUrl = null;
3857
return false;
3958
}
40-
url = value[..hashIndex];
41-
snippetKey = value[(hashIndex + 1)..];
59+
url = firstPart[..hashIndex];
60+
snippetKey = firstPart[(hashIndex + 1)..];
4261
return true;
4362
}
4463

@@ -61,25 +80,45 @@ public static bool ExtractSnippet(Line line, [NotNullWhen(true)] out string? key
6180
return true;
6281
}
6382

64-
public static bool ExtractWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey)
83+
public static bool ExtractWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey) =>
84+
ExtractWebSnippet(line, out url, out snippetKey, out _);
85+
86+
public static bool ExtractWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey, out string? viewUrl)
6587
{
6688
var lineCurrent = line.Current.TrimStart();
6789
if (!IsWebSnippetLine(lineCurrent))
6890
{
6991
url = null;
7092
snippetKey = null;
93+
viewUrl = null;
7194
return false;
7295
}
73-
var value = lineCurrent[12..].Trim(); // after 'web-snippet:', fixed from 11 to 12
74-
var hashIndex = value.LastIndexOf('#');
75-
if (hashIndex < 0 || hashIndex == value.Length - 1)
96+
var value = lineCurrent[12..].Trim(); // after 'web-snippet:'
97+
98+
// Check for optional second URL separated by whitespace
99+
var parts = value.Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
100+
string firstPart;
101+
if (parts.Length >= 2)
102+
{
103+
firstPart = parts[0];
104+
viewUrl = parts[1];
105+
}
106+
else
107+
{
108+
firstPart = value;
109+
viewUrl = null;
110+
}
111+
112+
var hashIndex = firstPart.LastIndexOf('#');
113+
if (hashIndex < 0 || hashIndex == firstPart.Length - 1)
76114
{
77115
url = null;
78116
snippetKey = null;
117+
viewUrl = null;
79118
return false;
80119
}
81-
url = value[..hashIndex];
82-
snippetKey = value[(hashIndex + 1)..];
120+
url = firstPart[..hashIndex];
121+
snippetKey = firstPart[(hashIndex + 1)..];
83122
return true;
84123
}
85124

src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ string GetSupText(Snippet snippet, string anchor)
8383
var path = pathLocal.Replace('\\', '/');
8484
if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
8585
{
86-
// For web-snippets: link back to the original URL with the snippet key as a hash
86+
// For web-snippets: use ViewUrl if provided, otherwise link back to the original URL with the snippet key as a hash
87+
if (snippet.ViewUrl != null)
88+
{
89+
return $"<a href='{snippet.ViewUrl}#L{snippet.StartLine}-L{snippet.EndLine}' title='Snippet source file'>anchor</a>";
90+
}
8791
return $"<a href='{path}#{snippet.Key}' title='Snippet source file'>anchor</a>";
8892
}
8993
if (!path.StartsWith(targetDirectory))

src/MarkdownSnippets/Reading/Snippet.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,39 @@ public static Snippet Build(int startLine, int endLine, string value, string key
4848
language = language,
4949
Path = path,
5050
ExpressiveCode = expressiveCode,
51-
Error = ""
51+
Error = "",
52+
ViewUrl = null
53+
};
54+
}
55+
56+
/// <summary>
57+
/// Initialise a new instance of <see cref="Snippet"/> with an optional view URL.
58+
/// </summary>
59+
public static Snippet Build(int startLine, int endLine, string value, string key, string language, string? path, string? expressiveCode, string? viewUrl)
60+
{
61+
Guard.AgainstNullAndEmpty(key, nameof(key));
62+
Guard.AgainstEmpty(path, nameof(path));
63+
Guard.AgainstEmpty(expressiveCode, nameof(expressiveCode));
64+
Guard.AgainstEmpty(viewUrl, nameof(viewUrl));
65+
Guard.AgainstUpperCase(language, nameof(language));
66+
if (language.StartsWith('.'))
67+
{
68+
throw new ArgumentException("Language cannot start with '.'", nameof(language));
69+
}
70+
71+
Guard.AgainstNegativeAndZero(startLine, nameof(startLine));
72+
Guard.AgainstNegativeAndZero(endLine, nameof(endLine));
73+
return new()
74+
{
75+
StartLine = startLine,
76+
EndLine = endLine,
77+
value = value,
78+
Key = key,
79+
language = language,
80+
Path = path,
81+
ExpressiveCode = expressiveCode,
82+
Error = "",
83+
ViewUrl = viewUrl
5284
};
5385
}
5486

@@ -67,6 +99,11 @@ public static Snippet Build(int startLine, int endLine, string value, string key
6799
/// </summary>
68100
public string? ExpressiveCode { get; private init; }
69101

102+
/// <summary>
103+
/// Optional URL to use for viewing the snippet source (for web-snippets).
104+
/// </summary>
105+
public string? ViewUrl { get; private init; }
106+
70107
/// <summary>
71108
/// The language of the snippet, extracted from the file extension of the input file.
72109
/// </summary>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
UsedSnippets: [
3+
{
4+
Key: snipPet,
5+
Language: txt,
6+
Value: Some code,
7+
Error: ,
8+
FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt(1-3),
9+
IsInError: false
10+
}
11+
],
12+
result:
13+
before
14+
15+
<!-- 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 -->
16+
```txt
17+
Some code
18+
```
19+
<!-- endSnippet -->
20+
21+
after
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
UsedSnippets: [
3+
{
4+
Key: snipPet,
5+
Language: txt,
6+
Value: Some code,
7+
Error: ,
8+
FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt(1-3),
9+
IsInError: false
10+
}
11+
],
12+
result:
13+
before
14+
15+
<!-- 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 -->
16+
```txt
17+
Some code
18+
```
19+
<!-- endSnippet -->
20+
21+
after
22+
}

src/Tests/MarkdownProcessor/MarkdownProcessorTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,46 @@ THAT SHOULD BE
814814
content);
815815
}
816816

817+
[Fact]
818+
public Task WithCommentWebSnippetWithViewUrl()
819+
{
820+
var content = """
821+
822+
before
823+
824+
<!-- 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 -->
825+
OLD CONTENT
826+
THAT SHOULD BE
827+
REPLACED
828+
<!-- endSnippet -->
829+
830+
after
831+
832+
""";
833+
834+
return SnippetVerifier.Verify(
835+
DocumentConvention.InPlaceOverwrite,
836+
content);
837+
}
838+
839+
[Fact]
840+
public Task WithInlineWebSnippetWithViewUrl()
841+
{
842+
var content = """
843+
844+
before
845+
846+
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
847+
848+
after
849+
850+
""";
851+
852+
return SnippetVerifier.Verify(
853+
DocumentConvention.SourceTransform,
854+
content);
855+
}
856+
817857
static Snippet SnippetBuild(string language, string key) =>
818858
Snippet.Build(
819859
language: language,

0 commit comments

Comments
 (0)