Browse Source

Handle anchor links

pull/22430/head
liangshiwei 10 months ago
parent
commit
e96f601641
  1. 2
      modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/DeletePdfFileInput.cs
  2. 2
      modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs
  3. 2
      modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/docs-admin-generate-proxy.json
  4. 2
      modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/generatePdf.js
  5. 2
      modules/docs/src/Volo.Docs.Admin.Web/wwwroot/client-proxies/docs-admin-proxy.js
  6. 14
      modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs
  7. 0
      modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Utils/UrlHelper.cs
  8. 2
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/BlobDocumentPdfFileStore.cs
  9. 1
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/DocsDocumentPdfGeneratorOptions.cs
  10. 113
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/DocumentPdfGenerator.cs
  11. 6
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/IText/ITextPdfRenderer.cs
  12. 65
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/Markdig/AnchorLinkRenderer.cs
  13. 27
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/Pdf/Markdig/AnchorLinkResolverExtension.cs

2
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; }

2
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));
}
}
}

2
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",

2
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);
}

2
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));

14
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<string> GetDocumentLinks(NavigationNode node, List<string> 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<DocumentParametersDto> GetParametersAsync(GetParametersDocumentInput input)
{
var project = await _projectRepository.GetAsync(input.ProjectId);

0
modules/docs/src/Volo.Docs.Web/Utils/UrlHelper.cs → modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Utils/UrlHelper.cs

2
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

1
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;
}

113
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<DocumentPdfGenerator> Logger { get; set; }
protected IDocumentSource DocumentSource { get; set; }
protected DocumentParams DocumentParams { get; set; }
protected List<PdfDocumentNode> AllPdfDocumentNodes { get; set; } = [];
protected MarkdownPipeline MarkdownPipeline { get; } = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
private readonly SemaphoreSlim _syncSemaphore;
protected List<PdfDocumentNode> AllPdfDocumentNodes { get; } = [];
public DocumentPdfGenerator(
IDocumentSourceFactory documentStoreFactory,
@ -53,33 +49,29 @@ public class DocumentPdfGenerator : IDocumentPdfGenerator, ITransientDependency
DocumentPdfFileStore = documentPdfFileStore;
PdfRenderer = pdfRenderer;
Logger = NullLogger<DocumentPdfGenerator>.Instance;
_syncSemaphore = new SemaphoreSlim(1, 1);
}
public virtual async Task<IRemoteStreamContent> 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<string> ConvertDocumentsToHtmlAsync(List<PdfDocumentNode> 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<string> 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<PdfDocumentNode> 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, @"(<img\s+[^>]*)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();
}
}

6
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<MemoryStream> GeneratePdfAsync(string htmlContent, List<PdfDocumentNode> pdfDocumentNodes)
public virtual async Task<MemoryStream> GeneratePdfAsync(string htmlContent, List<PdfDocumentNode> 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<PdfDocumentNode> pdfDocumentNodes)

65
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("<a href=\"#").Write(anchor).Write("\">");
renderer.Write(link.FirstChild?.ToString() ?? anchor);
renderer.Write("</a>");
}
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);
}
}

27
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<LinkInlineRenderer>(new AnchorLinkRenderer(_documentNode));
}
}
}
Loading…
Cancel
Save