Commit: 8b38979
Parent: 5396c57

Enable local links and images in markdown rendering

Mårten Åsberg committed on 2026-04-10 at 11:52
GitBrowser/Components/Pages/Repo.razor +6 -4
diff --git a/GitBrowser/Components/Pages/Repo.razor b/GitBrowser/Components/Pages/Repo.razor
index 2c02c76..f9b52c8 100644
@@ -24,7 +24,8 @@
<div class="dropdown-list">
@foreach (var branch in availableBranches.Where(b => b != currentBranch))
{
<a href="/@Uri.EscapeDataString(RepoName)/tree/@Uri.EscapeDataString(branch)" class="dropdown-item">@branch</a>
<a href="/@Uri.EscapeDataString(RepoName)/tree/@Uri.EscapeDataString(branch)"
class="dropdown-item">@branch</a>
}
</div>
</div>
@@ -90,7 +91,7 @@
</thead>
<tbody>
@foreach (var entry in entries.OrderByDescending(e => e.TargetType == TreeEntryTargetType.Tree).ThenBy(e =>
e.Name))
e.Name))
{
var entryPath = string.IsNullOrEmpty(displayPath) ? entry.Name : $"{displayPath}/{entry.Name}";
var entryUrl = $"/{Uri.EscapeDataString(RepoName)}/tree/{Uri.EscapeDataString(currentBranch)}/{entryPath}";
@@ -124,7 +125,7 @@
@if (commitInfo != null)
{
<a href="/@Uri.EscapeDataString(RepoName)/commit/@commitInfo.Sha"
title="@commitInfo.When.ToString("yyyy-MM-dd HH:mm:ss")">@HumanTime.GetRelativeTime(commitInfo.When)</a>
title="@commitInfo.When.ToString("yyyy-MM-dd HH:mm:ss")">@HumanTime.GetRelativeTime(commitInfo.When)</a>
}
else
{
@@ -140,7 +141,8 @@
@if (fileContent is not null)
{
<FilePreview IsReadme="@isReadme" FileName="@fileName" FileContent="@fileContent" />
<FilePreview IsReadme="@isReadme" Repo="@RepoName" BranchOrCommit="@currentBranch"
FilePath="@filePath" FileContent="@fileContent" />
}
}
</div>
GitBrowser/Components/Pages/Repo.razor.cs +3 -2
diff --git a/GitBrowser/Components/Pages/Repo.razor.cs b/GitBrowser/Components/Pages/Repo.razor.cs
index 9b88cf2..83c3b77 100644
@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GitBrowser.Services;
using LibGit2Sharp;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Options;
namespace GitBrowser.Components.Pages;
@@ -32,6 +30,7 @@ public sealed partial class Repo(
private TreeEntry[] entries = [];
private string currentBranch = "main";
private string currentPath = "";
private string filePath = "";
private string displayPath = "";
private string? cloneCommand;
private bool isViewingFile = false;
@@ -118,6 +117,7 @@ public sealed partial class Repo(
// Check if the path is a file or directory
if (!string.IsNullOrEmpty(currentPath))
{
filePath = currentPath;
var treeEntry = currentCommit[currentPath];
if (treeEntry is null)
@@ -202,6 +202,7 @@ public sealed partial class Repo(
fileContent = (Blob)readmeEntry.Target;
fileName = readmeEntry.Name;
isReadme = true;
filePath = readmeEntry.Path;
break;
}
}
GitBrowser/Components/Shared/FilePreview.razor +17 -16
diff --git a/GitBrowser/Components/Shared/FilePreview.razor b/GitBrowser/Components/Shared/FilePreview.razor
index 0d91740..1509fc1 100644
@@ -1,4 +1,5 @@
@using System.IO
@using GitBrowser.MarkdigExtensions
@using GitBrowser.SyntaxHighlighter
@using System.Web
@using System.Text
@@ -9,7 +10,7 @@
<div class="file-viewer @(IsReadme ? "readme-viewer" : "")">
<div class="file-viewer-header">
<span class="icon">@(IsReadme ? "📖" : "📄")</span>
<span class="file-name">@FileName</span>
<span class="file-name">@FilePath</span>
</div>
<div class="file-content">
@((MarkupString)filePreview)
@@ -17,15 +18,17 @@
</div>
@code {
private static readonly MarkdownPipeline markdownPipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
[Parameter]
public bool IsReadme { get; set; } = false;
[Parameter]
public required string FileName { get; set; }
public required string Repo { get; set; }
[Parameter]
public required string BranchOrCommit { get; set; }
[Parameter]
public required string FilePath { get; set; }
[Parameter]
public required Blob FileContent { get; set; }
@@ -37,21 +40,19 @@
protected override async Task OnInitializedAsync()
{
var extension = Path.GetExtension(FileName);
var extension = Path.GetExtension(FilePath);
if (MimeTypes.TryGetMimeType(FileName, out var mimeType) && mimeType.StartsWith("image/"))
if (MimeTypes.TryGetMimeType(FilePath, out var mimeType) && mimeType.StartsWith("image/"))
{
byte[] fileBytes;
using (var ms = new MemoryStream())
using (var stream = FileContent.GetContentStream())
{
await stream.CopyToAsync(ms);
fileBytes = ms.ToArray();
}
filePreview = (MarkupString)$"<img src=\"data:{mimeType};base64,{Convert.ToBase64String(fileBytes)}\">";
filePreview = (MarkupString)$"<img src=\"{Uri.EscapeDataString(Repo)}/raw/{BranchOrCommit}/{FilePath}\">";
}
else if (string.Equals(extension, ".md", StringComparison.OrdinalIgnoreCase))
{
var markdownPipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Use<LocalUrlRewriterExtension>(new(Repo, BranchOrCommit, FilePath))
.Build();
filePreview = (MarkupString)$"<div class=\"markdown\">{Markdown.ToHtml(FileContent.GetContentText(),
markdownPipeline)}</div>";
}
GitBrowser/Components/Shared/FilePreview.razor.css +5 -0
diff --git a/GitBrowser/Components/Shared/FilePreview.razor.css b/GitBrowser/Components/Shared/FilePreview.razor.css
index 6554de6..9d39998 100644
@@ -40,6 +40,11 @@
text-align: center;
}
.file-content ::deep img {
max-width: 100%;
height: auto;
}
.file-content ::deep .file-with-lines {
display: grid;
grid-template-columns: auto 1fr;
GitBrowser/MarkdigExtensions/LocalUrlRewriterExtension.cs +43 -0
diff --git a/GitBrowser/MarkdigExtensions/LocalUrlRewriterExtension.cs b/GitBrowser/MarkdigExtensions/LocalUrlRewriterExtension.cs
new file mode 100644
index 0000000..84eeea3
@@ -0,0 +1,43 @@
using System;
using System.IO;
using Markdig;
using Markdig.Renderers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace GitBrowser.MarkdigExtensions;
internal sealed class LocalUrlRewriterExtension(string repo, string branchOrCommit, string filePath)
: IMarkdownExtension
{
private readonly string path =
Path.GetDirectoryName(filePath) is not string path || string.IsNullOrWhiteSpace(path)
? ""
: path.TrimEnd('/') + "/";
public void Setup(MarkdownPipelineBuilder pipeline)
{
pipeline.DocumentProcessed -= OnDocumentProcessed;
pipeline.DocumentProcessed += OnDocumentProcessed;
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { }
private void OnDocumentProcessed(MarkdownDocument document)
{
foreach (var node in document.Descendants())
{
if (
node is not LinkInline link
|| string.IsNullOrWhiteSpace(link.Url)
|| !Uri.TryCreate(link.Url, UriKind.Relative, out var relativeUrl)
)
{
continue;
}
link.Url =
$"{Uri.EscapeDataString(repo)}/{(link.IsImage ? "raw" : "tree")}/{branchOrCommit}/{path}{relativeUrl}";
}
}
}
GitBrowser/Program.cs +35 -0
diff --git a/GitBrowser/Program.cs b/GitBrowser/Program.cs
index 0d9fb31..e1aaadf 100644
@@ -1,7 +1,10 @@
using GitBrowser;
using GitBrowser.Components;
using GitBrowser.Services;
using LibGit2Sharp;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -32,4 +35,36 @@ app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>();
app.MapGet(
"/{Repo}/raw/{BranchOrCommit}/{*Path}",
static (
[FromServices] RepositoryService repositoryService,
[FromRoute] string repo,
[FromRoute] string branchOrCommit,
[FromRoute] string path
) =>
{
if (
!repositoryService.TryGetRepository(repo, out var repository)
|| LookupCommit(repository.Repository, branchOrCommit) is not { } commit
|| commit[path] is not { } treeEntry
|| treeEntry.TargetType is not TreeEntryTargetType.Blob
)
{
return Results.NotFound();
}
return Results.File(
((Blob)treeEntry.Target).GetContentStream(),
contentType: MimeTypes.GetMimeType(treeEntry.Name),
fileDownloadName: treeEntry.Name,
lastModified: commit.Author.When,
enableRangeProcessing: true
);
static Commit? LookupCommit(Repository repo, string branchOrCommit) =>
repo.Branches[branchOrCommit]?.Tip ?? repo.Lookup<Commit>(branchOrCommit);
}
);
app.Run();