From e96f601641ab8a4bb7d704d3b9df2c00517d96f6 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Tue, 22 Apr 2025 14:20:47 +0800 Subject: [PATCH] Handle anchor links --- .../Docs/Admin/Projects/DeletePdfFileInput.cs | 2 +- .../Admin/Projects/ProjectAdminAppService.cs | 2 +- .../docs-admin-generate-proxy.json | 2 +- .../Pages/Docs/Admin/Projects/generatePdf.js | 2 + .../client-proxies/docs-admin-proxy.js | 2 +- .../Volo/Docs/Documents/DocumentAppService.cs | 14 +-- .../Volo/Docs}/Utils/UrlHelper.cs | 0 .../Documents/Pdf/BlobDocumentPdfFileStore.cs | 2 +- .../Pdf/DocsDocumentPdfGeneratorOptions.cs | 1 - .../Documents/Pdf/DocumentPdfGenerator.cs | 113 ++++++++++-------- .../Documents/Pdf/IText/ITextPdfRenderer.cs | 6 +- .../Pdf/Markdig/AnchorLinkRenderer.cs | 65 ++++++++++ .../Markdig/AnchorLinkResolverExtension.cs | 27 +++++ 13 files changed, 167 insertions(+), 71 deletions(-) rename modules/docs/src/{Volo.Docs.Web => Volo.Docs.Domain.Shared/Volo/Docs}/Utils/UrlHelper.cs (100%) create mode 100644 modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/Markdig/AnchorLinkRenderer.cs create mode 100644 modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/Markdig/AnchorLinkResolverExtension.cs diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/DeletePdfFileInput.cs b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/DeletePdfFileInput.cs index 074553cf46..6e8e30e57c 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/DeletePdfFileInput.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/DeletePdfFileInput.cs @@ -4,7 +4,7 @@ namespace Volo.Docs.Admin.Projects; public class DeletePdfFileInput { - public Guid Id { get; set; } + public Guid ProjectId { get; set; } public string Version { get; set; } diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs index 3ed75c4b84..a5d61aa358 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs @@ -185,7 +185,7 @@ namespace Volo.Docs.Admin.Projects public virtual async Task DeletePdfFileAsync(DeletePdfFileInput input) { - await _documentPdfCache.RemoveAsync(DocsDocumentPdfCacheItem.CalculateCacheKey(input.Id, input.Version, input.LanguageCode)); + await _documentPdfCache.RemoveAsync(DocsDocumentPdfCacheItem.CalculateCacheKey(input.ProjectId, input.Version, input.LanguageCode)); } } } diff --git a/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/docs-admin-generate-proxy.json b/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/docs-admin-generate-proxy.json index 36f81f8d46..96eddc28ee 100644 --- a/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/docs-admin-generate-proxy.json +++ b/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/docs-admin-generate-proxy.json @@ -1021,7 +1021,7 @@ "parameters": [ { "nameOnMethod": "input", - "name": "Id", + "name": "ProjectId", "jsonName": null, "type": "System.Guid", "typeSimple": "string", diff --git a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/generatePdf.js b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/generatePdf.js index 7be870f6b6..e65f978690 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/generatePdf.js +++ b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/generatePdf.js @@ -24,6 +24,7 @@ $(function () { $("#GeneratePdfBtn").click(function () { var $btn = $(this); $btn.buttonBusy(true); + $("#GenerateAndDownloadPdfBtn").buttonBusy(true); var input = { projectId: $("#ProjectId").val(), version: $("#Version").val(), @@ -36,6 +37,7 @@ $(function () { if (jqXHR.status === 200) { abp.message.success("PDF generated successfully."); $btn.buttonBusy(false); + $("#GenerateAndDownloadPdfBtn").buttonBusy(false); } else { abp.ajax.handleErrorStatusCode(jqXHR.status); } diff --git a/modules/docs/src/Volo.Docs.Admin.Web/wwwroot/client-proxies/docs-admin-proxy.js b/modules/docs/src/Volo.Docs.Admin.Web/wwwroot/client-proxies/docs-admin-proxy.js index 9afc61c17e..60bab5146a 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/wwwroot/client-proxies/docs-admin-proxy.js +++ b/modules/docs/src/Volo.Docs.Admin.Web/wwwroot/client-proxies/docs-admin-proxy.js @@ -138,7 +138,7 @@ volo.docs.admin.projectsAdmin.deletePdfFile = function(input, ajaxParams) { return abp.ajax($.extend(true, { - url: abp.appPath + 'api/docs/admin/projects/DeletePdfFile' + abp.utils.buildQueryString([{ name: 'id', value: input.id }, { name: 'version', value: input.version }, { name: 'languageCode', value: input.languageCode }]) + '', + url: abp.appPath + 'api/docs/admin/projects/DeletePdfFile' + abp.utils.buildQueryString([{ name: 'projectId', value: input.projectId }, { name: 'version', value: input.version }, { name: 'languageCode', value: input.languageCode }]) + '', type: 'DELETE', dataType: null }, ajaxParams)); diff --git a/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs b/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs index c156b669dd..e5bbca3f5a 100644 --- a/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs +++ b/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs @@ -17,6 +17,7 @@ using Volo.Docs.Caching; using Volo.Docs.Common.Projects; using Volo.Docs.Documents.FullSearch.Elastic; using Volo.Docs.Projects; +using Volo.Docs.Utils; using Volo.Extensions; namespace Volo.Docs.Documents @@ -282,7 +283,7 @@ namespace Volo.Docs.Documents private List GetDocumentLinks(NavigationNode node, List documentUrls, string prefix, string shortName, DocumentWithoutDetails document) { - if (!IsExternalLink(node.Path)) + if (!UrlHelper.IsExternalLink(node.Path)) { documentUrls.AddIfNotContains( NormalizePath(prefix, node.Path, shortName, document) @@ -320,17 +321,6 @@ namespace Volo.Docs.Documents : path; } - private static bool IsExternalLink(string path) - { - if (path.IsNullOrEmpty()) - { - return false; - } - - return path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || - path.StartsWith("https://", StringComparison.OrdinalIgnoreCase); - } - public virtual async Task GetParametersAsync(GetParametersDocumentInput input) { var project = await _projectRepository.GetAsync(input.ProjectId); diff --git a/modules/docs/src/Volo.Docs.Web/Utils/UrlHelper.cs b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Utils/UrlHelper.cs similarity index 100% rename from modules/docs/src/Volo.Docs.Web/Utils/UrlHelper.cs rename to modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Utils/UrlHelper.cs diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/BlobDocumentPdfFileStore.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/BlobDocumentPdfFileStore.cs index 9ddb076d3b..1415bf73f0 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/BlobDocumentPdfFileStore.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/BlobDocumentPdfFileStore.cs @@ -28,7 +28,7 @@ public class BlobDocumentPdfFileStore : IDocumentPdfFileStore, ITransientDepende public virtual async Task SetAsync(Project project, string version, string languageCode, Stream stream) { - await BlobContainer.SaveAsync(Options.Value.CalculatePdfFileName(project, version, languageCode), stream); + await BlobContainer.SaveAsync(Options.Value.CalculatePdfFileName(project, version, languageCode), stream, true); await Cache.SetAsync(DocsDocumentPdfCacheItem.CalculateCacheKey(project.Id, version, languageCode), new DocsDocumentPdfCacheItem(), new DistributedCacheEntryOptions diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/DocsDocumentPdfGeneratorOptions.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/DocsDocumentPdfGeneratorOptions.cs index 606eef2d4a..0b9dcfcca6 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/DocsDocumentPdfGeneratorOptions.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/DocsDocumentPdfGeneratorOptions.cs @@ -64,7 +64,6 @@ public class DocsDocumentPdfGeneratorOptions pre { position: relative; padding: 16px; - padding-top: calc(16px * 2.5); border-radius: 8px; border: 1px solid #e2e8f0; } diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/DocumentPdfGenerator.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/DocumentPdfGenerator.cs index fea0d488b4..f9b8185602 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/DocumentPdfGenerator.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/DocumentPdfGenerator.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; using System.Web; using Markdig; @@ -14,10 +13,10 @@ using Microsoft.Extensions.Options; using Volo.Abp; using Volo.Abp.Content; using Volo.Abp.DependencyInjection; -using Volo.Abp.Threading; -using Volo.Docs.Documents.Pdf.IText; +using Volo.Docs.Documents.Pdf.Markdig; using Volo.Docs.Documents.Rendering; using Volo.Docs.Projects; +using Volo.Docs.Utils; using Volo.Extensions; namespace Volo.Docs.Documents.Pdf; @@ -33,10 +32,7 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency protected ILogger Logger { get; set; } protected IDocumentSource DocumentSource { get; set; } protected DocumentParams DocumentParams { get; set; } - protected List AllPdfDocumentNodes { get; set; } = []; - protected MarkdownPipeline MarkdownPipeline { get; } = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); - - private readonly SemaphoreSlim _syncSemaphore; + protected List AllPdfDocumentNodes { get; } = []; public DocumentPdfGenerator( IDocumentSourceFactory documentStoreFactory, @@ -53,33 +49,29 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency DocumentPdfFileStore = documentPdfFileStore; PdfRenderer = pdfRenderer; Logger = NullLogger.Instance; - _syncSemaphore = new SemaphoreSlim(1, 1); } public virtual async Task GenerateAsync(Project project, string version, string languageCode) { - using (await _syncSemaphore.LockAsync()) - { - var fileName = Options.Value.CalculatePdfFileName(project, version, languageCode); - var fileStream = await DocumentPdfFileStore.GetOrNullAsync(project, version, languageCode); - - if (fileStream != null) - { - return new RemoteStreamContent(fileStream, fileName, "application/pdf"); - } - - DocumentSource = DocumentStoreFactory.Create(project.DocumentStoreType); - DocumentParams = await GetDocumentParamsAsync(project, version, languageCode); - var navigation = await GetNavigationAsync(project, version, languageCode); + var fileName = Options.Value.CalculatePdfFileName(project, version, languageCode); + var fileStream = await DocumentPdfFileStore.GetOrNullAsync(project, version, languageCode); - await SetAllPdfDocumentNodesAsync(navigation.Items, project, version, languageCode); - var htmlContent = await ConvertDocumentsToHtmlAsync(AllPdfDocumentNodes); + if (fileStream != null) + { + return new RemoteStreamContent(fileStream, fileName, "application/pdf"); + } - var pdfStream = await PdfRenderer.GeneratePdfAsync(htmlContent, AllPdfDocumentNodes); - await DocumentPdfFileStore.SetAsync(project, version, languageCode, pdfStream); + DocumentSource = DocumentStoreFactory.Create(project.DocumentStoreType); + DocumentParams = await GetDocumentParamsAsync(project, version, languageCode); + var navigation = await GetNavigationAsync(project, version, languageCode); + + await SetAllPdfDocumentNodesAsync(navigation.Items, project, version, languageCode); + var htmlContent = await ConvertDocumentsToHtmlAsync(AllPdfDocumentNodes); - return new RemoteStreamContent(pdfStream, fileName, "application/pdf"); - } + var pdfStream = await PdfRenderer.GeneratePdfAsync(htmlContent, AllPdfDocumentNodes); + await DocumentPdfFileStore.SetAsync(project, version, languageCode, pdfStream); + + return new RemoteStreamContent(pdfStream, fileName, "application/pdf"); } protected virtual async Task ConvertDocumentsToHtmlAsync(List pdfDocumentNodes) @@ -91,7 +83,7 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency if (pdfDocumentNode.Document != null) { var renderedDocument = await RenderDocumentAsync(pdfDocumentNode); - renderedDocument = NormalizeHtmlContent(pdfDocumentNode, Markdown.ToHtml(renderedDocument, MarkdownPipeline)); + renderedDocument = NormalizeHtmlContent(pdfDocumentNode, Markdown.ToHtml(renderedDocument, GetMarkdownPipeline(pdfDocumentNode))); contentBuilder.AppendLine(renderedDocument); } @@ -106,7 +98,8 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency protected virtual async Task RenderDocumentAsync(PdfDocumentNode pdfDocumentNode) { - var renderedDocument = await DocumentSectionRenderer.RenderAsync(pdfDocumentNode.Document.Content, pdfDocumentNode.RenderParameters); + var content = NormalizeMarkdownContent(pdfDocumentNode.Document.Content); + var renderedDocument = await DocumentSectionRenderer.RenderAsync(content, pdfDocumentNode.RenderParameters); if (pdfDocumentNode.RenderParameters != null) { renderedDocument = SetDocumentTitle(renderedDocument, pdfDocumentNode.Title); @@ -136,8 +129,8 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency Title = navigation.Text, IgnoreOnOutline = navigation.Path == Options.Value.CoverPagePath }; - - if (!navigation.Path.IsNullOrWhiteSpace()) + + if (!navigation.Path.IsNullOrWhiteSpace() && !navigation.HasChildItems) { var path = NormalizeNavigationPath(navigation.Path); var document = await GetDocumentAsync(project, path, version, languageCode); @@ -147,19 +140,12 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency pdfDocumentNode.Document = document; pdfDocumentNode.Title = GetDocumentTitle(navigation.Text, document.Content, firstParameterCombination); - pdfDocumentNode.Id = GetDocumentId(navigation.Path, firstParameterCombination); + pdfDocumentNode.Id = GetDocumentId(path, firstParameterCombination, true); pdfDocumentNode.RenderParameters = firstParameterCombination; - + if(parameters.Count <= 1) { - if (parentPdfDocumentNode == null) - { - AllPdfDocumentNodes.AddRange(parameterCombinationsDocuments); - } - else - { - parentPdfDocumentNode.Children.AddRange(parameterCombinationsDocuments); - } + AddParameterCombinationsDocuments(parentPdfDocumentNode, parameterCombinationsDocuments); } else { @@ -170,7 +156,7 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency { Document = document, Title = GetDocumentTitle(navigation.Text, document.Content, parameterCombination), - Id = GetDocumentId(navigation.Path, parameterCombination), + Id = GetDocumentId(path, parameterCombination, false), RenderParameters = parameterCombination }); } @@ -190,6 +176,11 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency { await SetAllPdfDocumentNodesAsync(navigation.Items, project, version, languageCode, pdfDocumentNode); } + + if (navigation == navigations.Last()) + { + AddParameterCombinationsDocuments(parentPdfDocumentNode, parameterCombinationsDocuments); + } } catch (Exception e) { @@ -198,6 +189,18 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency } } + private void AddParameterCombinationsDocuments(PdfDocumentNode parentPdfDocumentNode, List parameterCombinationsDocuments) + { + if (parentPdfDocumentNode == null) + { + AllPdfDocumentNodes.AddRange(parameterCombinationsDocuments); + } + else + { + parentPdfDocumentNode.Children.AddRange(parameterCombinationsDocuments); + } + } + private string NormalizeNavigationPath(string path) { return !path.EndsWith(".md") ? Path.Combine(path, "index.md") : path; @@ -330,15 +333,15 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency var paramValues = parameters.Select(x => $"{DocumentParams.Parameters.FirstOrDefault(p => p.Name == x.Key)?.DisplayName ?? x.Key}: {x.Value}").ToList(); return titleLine.TrimStart('#').Trim() + $" ({string.Join(", ", paramValues)})"; } - - private string GetDocumentId(string path, DocumentRenderParameters parameters) + private string GetDocumentId(string path, DocumentRenderParameters parameters, bool isFirstCombinationDocument) { var id = path.Replace(".md",string.Empty).Replace("/","-").Replace(" ", "-").ToLower(); - if (parameters != null) + if (parameters != null && !isFirstCombinationDocument) { id = $"{id}{parameters.Select(x => $"{x.Key}_{x.Value}").JoinAsString("-")}"; } + return id; } @@ -354,12 +357,12 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency { return Regex.Replace(htmlContent, @"(]*)src=""([^""]*)""([^>]*>)", delegate (Match match) { - if (IsExternalLink(match.Groups[2].Value)) + if (UrlHelper.IsExternalLink(match.Groups[2].Value)) { return match.Value; } - var rootUrl = IsExternalLink(pdfDocumentNode.Document.RawRootUrl) + var rootUrl = UrlHelper.IsExternalLink(pdfDocumentNode.Document.RawRootUrl) ? pdfDocumentNode.Document.RawRootUrl.EnsureEndsWith('/') : Options.Value.BaseUrl.EnsureEndsWith('/') + pdfDocumentNode.Document.RawRootUrl.TrimStart('/').EnsureEndsWith('/'); var newImageSource = rootUrl + @@ -370,10 +373,18 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency }, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Multiline); } - - private bool IsExternalLink(string link) + + private string NormalizeMarkdownContent(string content) + { + var pattern = @"````json\s*//$begin:math:display$doc-nav$end:math:display$[\s\S]*?````"; + return Regex.Replace(content, pattern, string.Empty, RegexOptions.IgnoreCase); + } + + private MarkdownPipeline GetMarkdownPipeline(PdfDocumentNode pdfDocumentNode) { - return link.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || - link.StartsWith("https://", StringComparison.OrdinalIgnoreCase); + return new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Use(new AnchorLinkResolverExtension(pdfDocumentNode)) + .Build(); } } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/IText/ITextPdfRenderer.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/IText/ITextPdfRenderer.cs index f4164dfd3a..133d8b008f 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/IText/ITextPdfRenderer.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/IText/ITextPdfRenderer.cs @@ -21,7 +21,7 @@ public class ITextPdfRenderer : IPdfRenderer ,ITransientDependency Options = options; } - public virtual Task GeneratePdfAsync(string htmlContent, List pdfDocumentNodes) + public virtual async Task GeneratePdfAsync(string htmlContent, List pdfDocumentNodes) { var pdfStream = new MemoryStream(); var pdfWrite = new PdfWriter(pdfStream); @@ -38,6 +38,8 @@ public class ITextPdfRenderer : IPdfRenderer ,ITransientDependency var tagWorkerFactory = new HtmlIdTagWorkerFactory(pdfDocument); converter.SetTagWorkerFactory(tagWorkerFactory); + + await File.WriteAllTextAsync("/Users/liangshiweis/codes/test.html", htmlBuilder.ToString()); HtmlConverter.ConvertToDocument(htmlBuilder.ToString(), pdfDocument, converter); tagWorkerFactory.AddNamedDestinations(); @@ -46,7 +48,7 @@ public class ITextPdfRenderer : IPdfRenderer ,ITransientDependency textDocument.Close(); pdfStream.Position = 0; - return Task.FromResult(pdfStream); + return pdfStream; } private void BuildPdfOutlines(PdfOutline parentOutline, List pdfDocumentNodes) diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/Markdig/AnchorLinkRenderer.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/Markdig/AnchorLinkRenderer.cs new file mode 100644 index 0000000000..7f8d65d4ff --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/Markdig/AnchorLinkRenderer.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Markdig.Renderers; +using Markdig.Renderers.Html.Inlines; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using Volo.Docs.Utils; + +namespace Volo.Docs.Documents.Pdf.Markdig; + +public class AnchorLinkRenderer : LinkInlineRenderer +{ + private readonly PdfDocumentNode _documentNode; + + public AnchorLinkRenderer(PdfDocumentNode documentNode) + { + _documentNode = documentNode; + } + + protected override void Write(HtmlRenderer renderer, LinkInline link) + { + if (UrlHelper.IsExternalLink(link.Url) || link.Url.IsNullOrWhiteSpace() || link.IsImage) + { + base.Write(renderer, link); + return; + } + + var anchor = ResolveRelativeMarkdownPath(_documentNode.Document.Name, link.Url) + .Replace(".md",string.Empty).Replace("/","-").Replace(" ", "-").ToLower(); + + renderer.Write(""); + renderer.Write(link.FirstChild?.ToString() ?? anchor); + renderer.Write(""); + } + + private string ResolveRelativeMarkdownPath(string currentPath, string relativePath) + { + currentPath = currentPath.EnsureStartsWith('/'); + relativePath = relativePath.EnsureStartsWith('/'); + + var currentDir = currentPath.Substring(0, currentPath.LastIndexOf('/')); + + var baseParts = currentDir.Split('/', StringSplitOptions.RemoveEmptyEntries).ToList(); + var relativeParts = relativePath.Split('/', StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in relativeParts) + { + if (part == "..") + { + if (baseParts.Count > 0) + { + baseParts.RemoveAt(baseParts.Count - 1); + } + } + else if (part != ".") + { + baseParts.Add(part); + } + } + + return string.Join("/", baseParts); + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/Markdig/AnchorLinkResolverExtension.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/Markdig/AnchorLinkResolverExtension.cs new file mode 100644 index 0000000000..d8a66b368a --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/Markdig/AnchorLinkResolverExtension.cs @@ -0,0 +1,27 @@ +using Markdig; +using Markdig.Renderers; +using Markdig.Renderers.Html.Inlines; + +namespace Volo.Docs.Documents.Pdf.Markdig; + +public class AnchorLinkResolverExtension : IMarkdownExtension +{ + private readonly PdfDocumentNode _documentNode; + + public AnchorLinkResolverExtension(PdfDocumentNode documentNode) + { + _documentNode = documentNode; + } + + public void Setup(MarkdownPipelineBuilder pipeline) + { + } + + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) + { + htmlRenderer.ObjectRenderers.Replace(new AnchorLinkRenderer(_documentNode)); + } + } +} \ No newline at end of file