mirror of https://github.com/abpframework/abp.git
39 changed files with 1171 additions and 459 deletions
@ -1,6 +1,6 @@ |
|||
using Volo.Abp.BlobStoring; |
|||
|
|||
namespace Volo.Docs.Common; |
|||
namespace Volo.Docs.Common.Documents; |
|||
|
|||
[BlobContainerName("docs-document-pdf")] |
|||
public class DocsDocumentPdfContainer |
|||
@ -0,0 +1,64 @@ |
|||
namespace Volo.Docs.Common.Documents; |
|||
|
|||
public class DocsDocumentPdfGeneratorOptions |
|||
{ |
|||
public string HtmlLayout { get; set; } |
|||
public string HtmlStyles { get; set; } |
|||
|
|||
public string BaseUrl { get; set; } |
|||
|
|||
public DocsDocumentPdfGeneratorOptions() |
|||
{ |
|||
HtmlLayout = @"
|
|||
<!DOCTYPE html> |
|||
<head> |
|||
<meta charset='utf-8' /> |
|||
<style> |
|||
{{style-placeholder}} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
{{content-placeholder}} |
|||
</body> |
|||
</html>";
|
|||
|
|||
HtmlStyles = @"
|
|||
body { margin: 20px; line-height: 1.6; font-family: Arial, sans-serif;} |
|||
a { text-decoration: none; } |
|||
.page { |
|||
page-break-after: always; |
|||
margin-bottom: 30px; |
|||
padding: 15px; |
|||
} |
|||
img { |
|||
max-width:100%; |
|||
height: auto; |
|||
display: block; |
|||
} |
|||
code, pre { |
|||
background: #f8fafc; |
|||
white-space: pre-wrap; |
|||
word-wrap: break-word; |
|||
} |
|||
pre code { padding: 0; border: none; } |
|||
code { |
|||
padding: 2px 6px; |
|||
border-radius: 4px; |
|||
border: 1px solid #e2e8f0; |
|||
font-family: Consolas, monospace; |
|||
} |
|||
pre { |
|||
position: relative; |
|||
padding: 16px; |
|||
padding-top: calc(16px * 2.5); |
|||
border-radius: 8px; |
|||
border: 1px solid #e2e8f0; |
|||
} |
|||
blockquote { |
|||
border-left: 4px solid #e2e8f0; |
|||
margin: 20px 0; |
|||
padding: 10px 20px; |
|||
background: #f8fafc; |
|||
}";
|
|||
} |
|||
} |
|||
@ -0,0 +1,419 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Text.RegularExpressions; |
|||
using System.Threading.Tasks; |
|||
using System.Web; |
|||
using iText.Html2pdf; |
|||
using iText.Kernel.Pdf; |
|||
using iText.Kernel.Pdf.Action; |
|||
using Markdig; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Content; |
|||
using Volo.Docs.Documents; |
|||
using Volo.Docs.Documents.Rendering; |
|||
using Volo.Docs.Projects; |
|||
using Volo.Extensions; |
|||
using ITextDocument = iText.Layout.Document; |
|||
|
|||
namespace Volo.Docs.Common.Documents; |
|||
|
|||
public class DocumentPdfGenerator : IDocumentPdfGenerator |
|||
{ |
|||
protected IDocumentSourceFactory DocumentStoreFactory { get; } |
|||
protected IDocumentRepository DocumentRepository { get; } |
|||
protected IDocumentSectionRenderer DocumentSectionRenderer { get; } |
|||
protected IOptions<DocsDocumentPdfGeneratorOptions> Options { get; } |
|||
protected ILogger<DocumentPdfGenerator> Logger { get; set; } |
|||
protected IDocumentSource DocumentSource { get; set; } |
|||
protected DocumentParams DocumentParams { get; set; } |
|||
protected List<PdfDocumentNode> AllPdfDocumentNodes { get; set; } = new(); |
|||
protected MarkdownPipeline MarkdownPipeline { get; } = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); |
|||
|
|||
public DocumentPdfGenerator( |
|||
IDocumentSourceFactory documentStoreFactory, |
|||
IDocumentRepository documentRepository, |
|||
IOptions<DocsDocumentPdfGeneratorOptions> options, |
|||
IDocumentSectionRenderer documentSectionRenderer) |
|||
{ |
|||
DocumentStoreFactory = documentStoreFactory; |
|||
DocumentRepository = documentRepository; |
|||
Options = options; |
|||
DocumentSectionRenderer = documentSectionRenderer; |
|||
Logger = NullLogger<DocumentPdfGenerator>.Instance; |
|||
} |
|||
|
|||
public virtual async Task<IRemoteStreamContent> GenerateAsync(Project project, string version, string languageCode) |
|||
{ |
|||
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); |
|||
|
|||
var pdfStream = await GeneratePdfAsync(htmlContent); |
|||
|
|||
return new RemoteStreamContent(pdfStream, CalculateDocumentPdfFileName(project.ShortName, version, languageCode), "application/pdf"); |
|||
} |
|||
|
|||
protected virtual async Task<string> ConvertDocumentsToHtmlAsync(List<PdfDocumentNode> pdfDocumentNodes) |
|||
{ |
|||
var contentBuilder = new StringBuilder(); |
|||
|
|||
foreach (var pdfDocumentNode in pdfDocumentNodes) |
|||
{ |
|||
if (pdfDocumentNode.Document != null) |
|||
{ |
|||
var renderedDocument = await RenderDocumentAsync(pdfDocumentNode); |
|||
renderedDocument = NormalizeHtmlContent(pdfDocumentNode, Markdown.ToHtml(renderedDocument, MarkdownPipeline)); |
|||
contentBuilder.AppendLine(renderedDocument); |
|||
} |
|||
|
|||
if (pdfDocumentNode.HasChildren) |
|||
{ |
|||
contentBuilder.AppendLine(await ConvertDocumentsToHtmlAsync(pdfDocumentNode.Children)); |
|||
} |
|||
} |
|||
|
|||
return contentBuilder.ToString(); |
|||
} |
|||
|
|||
protected virtual Task<MemoryStream> GeneratePdfAsync(string htmlContent) |
|||
{ |
|||
var pdfStream = new MemoryStream(); |
|||
var pdfWrite = new PdfWriter(pdfStream); |
|||
var pdfDocument = new PdfDocument(pdfWrite); |
|||
var textDocument = new ITextDocument(pdfDocument); |
|||
pdfWrite.SetCloseStream(false); |
|||
|
|||
var htmlBuilder = new StringBuilder(); |
|||
htmlBuilder.Append(Options.Value.HtmlLayout); |
|||
htmlBuilder.Replace("{{style-placeholder}}", Options.Value.HtmlStyles); |
|||
htmlBuilder.Replace("{{content-placeholder}}", htmlContent); |
|||
|
|||
var converter = new ConverterProperties(); |
|||
var tagWorkerFactory = new HtmlIdTagWorkerFactory(pdfDocument); |
|||
converter.SetTagWorkerFactory(tagWorkerFactory); |
|||
|
|||
HtmlConverter.ConvertToDocument(htmlBuilder.ToString(), pdfDocument, converter); |
|||
|
|||
tagWorkerFactory.AddNamedDestinations(); |
|||
var pdfOutlines = pdfDocument.GetOutlines(false); |
|||
BuildPdfOutlines(pdfOutlines, AllPdfDocumentNodes); |
|||
|
|||
textDocument.Close(); |
|||
pdfStream.Position = 0; |
|||
return Task.FromResult(pdfStream); |
|||
} |
|||
|
|||
private void BuildPdfOutlines(PdfOutline parentOutline, List<PdfDocumentNode> pdfDocumentNodes) |
|||
{ |
|||
foreach (var pdfDocumentNode in pdfDocumentNodes) |
|||
{ |
|||
if (pdfDocumentNode.IgnoreOnOutline) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var outline = parentOutline.AddOutline(pdfDocumentNode.Title); |
|||
if (!pdfDocumentNode.Id.IsNullOrWhiteSpace()) |
|||
{ |
|||
outline.AddAction(PdfAction.CreateGoTo(pdfDocumentNode.Id)); |
|||
} |
|||
|
|||
if (pdfDocumentNode.HasChildren) |
|||
{ |
|||
BuildPdfOutlines(outline, pdfDocumentNode.Children); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task<string> RenderDocumentAsync(PdfDocumentNode pdfDocumentNode) |
|||
{ |
|||
var renderedDocument = await DocumentSectionRenderer.RenderAsync(pdfDocumentNode.Document.Content, pdfDocumentNode.RenderParameters); |
|||
if (pdfDocumentNode.RenderParameters != null) |
|||
{ |
|||
renderedDocument = SetDocumentTitle(renderedDocument, pdfDocumentNode.Title); |
|||
} |
|||
|
|||
return renderedDocument; |
|||
} |
|||
|
|||
protected virtual async Task SetAllPdfDocumentNodesAsync( |
|||
List<NavigationNode> navigations, |
|||
Project project, |
|||
string version, |
|||
string languageCode, |
|||
PdfDocumentNode parentPdfDocumentNode = null) |
|||
{ |
|||
var parameterCombinationsDocuments = new List<PdfDocumentNode>(); |
|||
foreach (var navigation in navigations) |
|||
{ |
|||
if (navigation.IgnoreOnDownload) |
|||
{ |
|||
continue; |
|||
} |
|||
try |
|||
{ |
|||
var pdfDocumentNode = new PdfDocumentNode |
|||
{ |
|||
Title = navigation.Text, |
|||
IgnoreOnOutline = navigation.Path == "index.md" |
|||
}; |
|||
|
|||
if (!navigation.Path.IsNullOrWhiteSpace()) |
|||
{ |
|||
var path = NormalizeNavigationPath(navigation.Path); |
|||
var document = await GetDocumentAsync(project, path, version, languageCode); |
|||
var parameters = await DocumentSectionRenderer.GetAvailableParametersAsync(document.Content); |
|||
var parameterCombinations = GenerateAllParameterCombinations(parameters.Keys.ToList(), parameters); |
|||
var firstParameterCombination = parameterCombinations.FirstOrDefault(); |
|||
|
|||
pdfDocumentNode.Document = document; |
|||
pdfDocumentNode.Title = GetDocumentTitle(navigation.Text, document.Content, firstParameterCombination); |
|||
pdfDocumentNode.Id = GetDocumentId(navigation.Path, firstParameterCombination); |
|||
pdfDocumentNode.RenderParameters = firstParameterCombination; |
|||
|
|||
if(parameters.Count <= 1) |
|||
{ |
|||
if (parentPdfDocumentNode == null) |
|||
{ |
|||
AllPdfDocumentNodes.AddRange(parameterCombinationsDocuments); |
|||
} |
|||
else |
|||
{ |
|||
parentPdfDocumentNode.Children.AddRange(parameterCombinationsDocuments); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
for (var i = 1; i < parameterCombinations.Count; i++) |
|||
{ |
|||
var parameterCombination = parameterCombinations[i]; |
|||
parameterCombinationsDocuments.Add(new PdfDocumentNode |
|||
{ |
|||
Document = document, |
|||
Title = GetDocumentTitle(navigation.Text, document.Content, parameterCombination), |
|||
Id = GetDocumentId(navigation.Path, parameterCombination), |
|||
RenderParameters = parameterCombination |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (parentPdfDocumentNode == null) |
|||
{ |
|||
AllPdfDocumentNodes.Add(pdfDocumentNode); |
|||
} |
|||
else |
|||
{ |
|||
parentPdfDocumentNode.Children.Add(pdfDocumentNode); |
|||
} |
|||
|
|||
if (navigation.HasChildItems) |
|||
{ |
|||
await SetAllPdfDocumentNodesAsync(navigation.Items, project, version, languageCode, pdfDocumentNode); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Logger.LogWarning(e, $"Cannot get document for the path '{navigation.Path}' in the project {project.Name}."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected virtual string CalculateDocumentPdfFileName(string projectName, string version, string languageCode) |
|||
{ |
|||
return $"{projectName.ToLower()}-{languageCode.ToLower()}.pdf"; |
|||
} |
|||
|
|||
private string NormalizeNavigationPath(string path) |
|||
{ |
|||
return !path.EndsWith(".md") ? Path.Combine(path, "index.md") : path; |
|||
} |
|||
|
|||
private async Task<NavigationNode> GetNavigationAsync( |
|||
Project project, |
|||
string version, |
|||
string languageCode) |
|||
{ |
|||
var navigationDocument = await GetDocumentAsync(project, project.NavigationDocumentName, version, languageCode); |
|||
|
|||
if (!DocsJsonSerializerHelper.TryDeserialize<NavigationNode>(navigationDocument.Content, out var navigation)) |
|||
{ |
|||
throw new UserFriendlyException($"Cannot validate navigation file '{project.NavigationDocumentName}' for the project {project.Name}."); |
|||
} |
|||
|
|||
navigation.Items.Insert(0, new NavigationNode |
|||
{ |
|||
Text = "Index", |
|||
Path = "index.md" |
|||
}); |
|||
|
|||
return navigation; |
|||
} |
|||
|
|||
private async Task<DocumentParams> GetDocumentParamsAsync( |
|||
Project project, |
|||
string version, |
|||
string languageCode) |
|||
{ |
|||
if (project.ParametersDocumentName.IsNullOrWhiteSpace()) |
|||
{ |
|||
return new DocumentParams(); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var documentParamsDocument = await GetDocumentAsync(project, project.ParametersDocumentName, version, languageCode); |
|||
|
|||
if (!DocsJsonSerializerHelper.TryDeserialize<DocumentParams>(documentParamsDocument.Content, out var documentParams)) |
|||
{ |
|||
throw new UserFriendlyException($"Cannot validate document params file '{project.ParametersDocumentName}' for the project {project.Name}."); |
|||
} |
|||
|
|||
return documentParams; |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Logger.LogError(e, $"Cannot get document params for the project {project.Name}.", e); |
|||
return new DocumentParams(); |
|||
} |
|||
} |
|||
|
|||
private async Task<Document> GetDocumentAsync( |
|||
Project project, |
|||
string documentName, |
|||
string version, |
|||
string languageCode) |
|||
{ |
|||
version = string.IsNullOrWhiteSpace(version) ? project.LatestVersionBranchName : version; |
|||
var document = await DocumentRepository.FindAsync(project.Id, documentName, version, languageCode); |
|||
|
|||
if (document != null) |
|||
{ |
|||
return document; |
|||
} |
|||
|
|||
document = await DocumentSource.GetDocumentAsync(project, documentName, languageCode, version); |
|||
|
|||
return document; |
|||
} |
|||
|
|||
protected virtual List<DocumentRenderParameters> GenerateAllParameterCombinations(List<string> parameterKeys, Dictionary<string, List<string>> parameters) |
|||
{ |
|||
var parameterCombinations = new List<DocumentRenderParameters>(); |
|||
|
|||
if (parameters.Count <= 0) |
|||
{ |
|||
return parameterCombinations; |
|||
} |
|||
|
|||
GenerateCombinations(0, new DocumentRenderParameters()); |
|||
return parameterCombinations; |
|||
|
|||
void GenerateCombinations(int keyIndex, DocumentRenderParameters currentCombination) |
|||
{ |
|||
if (keyIndex == parameterKeys.Count) |
|||
{ |
|||
parameterCombinations.Add(new DocumentRenderParameters(currentCombination)); |
|||
return; |
|||
} |
|||
|
|||
var currentKey = parameterKeys[keyIndex]; |
|||
foreach (var value in parameters[currentKey]) |
|||
{ |
|||
currentCombination[currentKey] = value; |
|||
GenerateCombinations(keyIndex + 1, currentCombination); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private string SetDocumentTitle(string content, string title) |
|||
{ |
|||
var titleLine = content.Split('\n').FirstOrDefault(x => x.TrimStart().StartsWith("#")); |
|||
if (titleLine == null) |
|||
{ |
|||
return content; |
|||
} |
|||
|
|||
var newTitle = $"# {title}"; |
|||
return content.Replace(titleLine, newTitle); |
|||
} |
|||
|
|||
private string GetDocumentTitle(string title, string content, DocumentRenderParameters parameters) |
|||
{ |
|||
if (parameters == null || parameters.Count <= 0) |
|||
{ |
|||
return title; |
|||
} |
|||
|
|||
var titleLine = content.Split('\n').FirstOrDefault(x => x.TrimStart().StartsWith("#")); |
|||
if (titleLine == null) |
|||
{ |
|||
return title; |
|||
} |
|||
|
|||
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) |
|||
{ |
|||
var id = path.Replace(".md",string.Empty).Replace("/","-").Replace(" ", "-").ToLower(); |
|||
if (parameters != null) |
|||
{ |
|||
id = $"{id}{parameters.Select(x => $"{x.Key}_{x.Value}").JoinAsString("-")}"; |
|||
} |
|||
return id; |
|||
} |
|||
|
|||
private string NormalizeHtmlContent(PdfDocumentNode pdfDocumentNode, string htmlContent) |
|||
{ |
|||
htmlContent = $"<div class='page' id='{pdfDocumentNode.Id}'>{htmlContent}</div>"; |
|||
|
|||
htmlContent = Regex.Replace(htmlContent, @"(<img\s+[^>]*)src=""([^""]*)""([^>]*>)", delegate (Match match) |
|||
{ |
|||
if (IsExternalLink(match.Groups[2].Value)) |
|||
{ |
|||
return match.Value; |
|||
} |
|||
|
|||
var rootUrl = IsExternalLink(pdfDocumentNode.Document.RawRootUrl) |
|||
? pdfDocumentNode.Document.RawRootUrl.EnsureEndsWith('/') |
|||
: Options.Value.BaseUrl.EnsureEndsWith('/') + pdfDocumentNode.Document.RawRootUrl.TrimStart('/').EnsureEndsWith('/'); |
|||
var newImageSource = rootUrl + |
|||
(pdfDocumentNode.Document.LocalDirectory.IsNullOrEmpty() ? "" : pdfDocumentNode.Document.LocalDirectory.TrimStart('/').EnsureEndsWith('/')) + |
|||
match.Groups[2].Value.TrimStart('/'); |
|||
|
|||
return match.Groups[1] + " src=\"" + HttpUtility.HtmlEncode(newImageSource) + "\" " + match.Groups[3]; |
|||
|
|||
}, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Multiline); |
|||
|
|||
return htmlContent; |
|||
} |
|||
|
|||
private bool IsExternalLink(string link) |
|||
{ |
|||
return link.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || |
|||
link.StartsWith("https://", StringComparison.OrdinalIgnoreCase); |
|||
} |
|||
|
|||
protected class PdfDocumentNode |
|||
{ |
|||
public Document Document { get; set; } |
|||
public string Title { get; set; } |
|||
public string Id { get; set; } |
|||
public List<PdfDocumentNode> Children { get; set; } = []; |
|||
public DocumentRenderParameters RenderParameters { get; set; } |
|||
public bool HasChildren => Children.Any(); |
|||
public bool IgnoreOnOutline { get; set; } |
|||
} |
|||
} |
|||
@ -1,88 +1,28 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.BlobStoring; |
|||
using Volo.Abp.Content; |
|||
using Volo.Docs.Documents; |
|||
using Volo.Docs.Projects; |
|||
using Volo.Extensions; |
|||
|
|||
namespace Volo.Docs.Common.Documents; |
|||
|
|||
[Authorize(DocsCommonPermissions.Documents.PdfGeneration)] |
|||
public class DocumentPdfGeneratorAppService : ApplicationService, IDocumentPdfGeneratorAppService |
|||
{ |
|||
protected IBlobContainer<DocsDocumentPdfContainer> BlobContainer { get; } |
|||
protected IProjectRepository ProjectRepository { get; } |
|||
protected IDocumentSourceFactory DocumentStoreFactory { get; } |
|||
protected IDocumentRepository DocumentRepository { get; } |
|||
|
|||
private readonly IDocumentPdfGenerator _documentPdfGenerator; |
|||
private readonly IProjectRepository _projectRepository; |
|||
|
|||
public DocumentPdfGeneratorAppService( |
|||
IBlobContainer<DocsDocumentPdfContainer> blobContainer, |
|||
IProjectRepository projectRepository, |
|||
IDocumentSourceFactory documentStoreFactory, |
|||
IDocumentRepository documentRepository) |
|||
IDocumentPdfGenerator documentPdfGenerator, |
|||
IProjectRepository projectRepository) |
|||
{ |
|||
BlobContainer = blobContainer; |
|||
ProjectRepository = projectRepository; |
|||
DocumentStoreFactory = documentStoreFactory; |
|||
DocumentRepository = documentRepository; |
|||
_documentPdfGenerator = documentPdfGenerator; |
|||
_projectRepository = projectRepository; |
|||
} |
|||
|
|||
public virtual async Task<IRemoteStreamContent> GeneratePdfAsync(DocumentPdfGeneratorInput input) |
|||
{ |
|||
var project = await ProjectRepository.GetAsync(input.ProjectId); |
|||
var fileName = CalculateDocumentPdfFileName(project.Name, input.Version, input.LanguageCode); |
|||
var stream = await BlobContainer.GetOrNullAsync(fileName); |
|||
|
|||
if(stream != null) |
|||
{ |
|||
return new RemoteStreamContent(stream, fileName , "application/pdf"); |
|||
} |
|||
|
|||
var documentStore = DocumentStoreFactory.Create(project.DocumentStoreType); |
|||
var navigation = await GetNavigationAsync(documentStore, project, input.LanguageCode, input.Version); |
|||
|
|||
|
|||
} |
|||
|
|||
protected virtual string CalculateDocumentPdfFileName(string projectName, string version, string languageCode) |
|||
{ |
|||
return $"{projectName.ToLower()}-{version.ToLower()}--{languageCode.ToLower()}"; |
|||
} |
|||
|
|||
private async Task<NavigationNode> GetNavigationAsync( |
|||
IDocumentSource documentStore, |
|||
Project project, |
|||
string languageCode, |
|||
string version) |
|||
public async Task<IRemoteStreamContent> GeneratePdfAsync(DocumentPdfGeneratorInput input) |
|||
{ |
|||
var navigationDocument = await GetDocumentAsync(documentStore, project, project.NavigationDocumentName, languageCode, version); |
|||
|
|||
if (!DocsJsonSerializerHelper.TryDeserialize<NavigationNode>(navigationDocument.Content, out var navigation)) |
|||
{ |
|||
throw new UserFriendlyException($"Cannot validate navigation file '{project.NavigationDocumentName}' for the project {project.Name}."); |
|||
} |
|||
|
|||
return navigation; |
|||
} |
|||
|
|||
private async Task<Document> GetDocumentAsync( |
|||
IDocumentSource documentStore, |
|||
Project project, |
|||
string documentName, |
|||
string languageCode, |
|||
string version) |
|||
{ |
|||
version = string.IsNullOrWhiteSpace(version) ? project.LatestVersionBranchName : version; |
|||
var document = await DocumentRepository.FindAsync(project.Id, documentName, version, languageCode); |
|||
|
|||
if (document != null) |
|||
{ |
|||
return document; |
|||
} |
|||
|
|||
document = await documentStore.GetDocumentAsync(project, documentName, languageCode, version); |
|||
|
|||
return document; |
|||
var project = await _projectRepository.GetAsync(input.ProjectId); |
|||
return await _documentPdfGenerator.GenerateAsync(project, input.Version, input.LanguageCode); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
using System.Collections.Generic; |
|||
using iText.Html2pdf.Attach; |
|||
using iText.Html2pdf.Attach.Impl; |
|||
using iText.Kernel.Pdf; |
|||
using iText.Kernel.Pdf.Navigation; |
|||
using iText.StyledXmlParser.Node; |
|||
|
|||
namespace Volo.Docs.Common.Documents; |
|||
|
|||
public class HtmlIdTagWorkerFactory: DefaultTagWorkerFactory |
|||
{ |
|||
private readonly PdfDocument _pdfDocument; |
|||
private readonly Dictionary<string, int> _pageDestinations = new(); |
|||
|
|||
public HtmlIdTagWorkerFactory(PdfDocument pdfDocument) |
|||
{ |
|||
_pdfDocument = pdfDocument; |
|||
} |
|||
|
|||
public override ITagWorker GetCustomTagWorker(IElementNode tag, ProcessorContext context) |
|||
{ |
|||
var tagClass = tag.GetAttribute("class"); |
|||
if (tag.Name().Equals("div") && tagClass =="page") |
|||
{ |
|||
var id = tag.GetAttribute("id"); |
|||
_pageDestinations[id] = _pdfDocument.GetNumberOfPages() + 1; |
|||
} |
|||
|
|||
return base.GetCustomTagWorker(tag, context); |
|||
} |
|||
|
|||
public void AddNamedDestinations() |
|||
{ |
|||
foreach (var pageDestination in _pageDestinations) |
|||
{ |
|||
var page = _pdfDocument.GetPage(pageDestination.Value); |
|||
var destination = PdfExplicitDestination.CreateFit(page); |
|||
_pdfDocument.AddNamedDestination(pageDestination.Key, destination.GetPdfObject()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Content; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Docs.Projects; |
|||
|
|||
namespace Volo.Docs.Common.Documents; |
|||
|
|||
public interface IDocumentPdfGenerator : ITransientDependency |
|||
{ |
|||
Task<IRemoteStreamContent> GenerateAsync(Project project, string version, string languageCode); |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks> |
|||
<AssemblyName>Volo.Docs.Common.HttpApi.Client</AssemblyName> |
|||
<PackageId>Volo.Docs.Common.HttpApi.Client</PackageId> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Docs.Common.Application.Contracts\Volo.Docs.Common.Application.Contracts.csproj" /> |
|||
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Http.Client\Volo.Abp.Http.Client.csproj" /> |
|||
|
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,18 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net9.0</TargetFramework> |
|||
<AssemblyName>Volo.Docs.Common.HttpApi</AssemblyName> |
|||
<PackageId>Volo.Docs.Common.HttpApi</PackageId> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Docs.Common.Application.Contracts\Volo.Docs.Common.Application.Contracts.csproj" /> |
|||
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.Mvc\Volo.Abp.AspNetCore.Mvc.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,35 @@ |
|||
using Localization.Resources.AbpUi; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Docs.Common; |
|||
using Volo.Docs.Localization; |
|||
|
|||
namespace Volo.Docs |
|||
{ |
|||
[DependsOn( |
|||
typeof(DocsCommonApplicationContractsModule), |
|||
typeof(AbpAspNetCoreMvcModule) |
|||
)] |
|||
public class DocsCommonHttpApiModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
PreConfigure<IMvcBuilder>(mvcBuilder => |
|||
{ |
|||
mvcBuilder.AddApplicationPartIfNotExists(typeof(DocsCommonHttpApiModule).Assembly); |
|||
}); |
|||
} |
|||
|
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<DocsResource>() |
|||
.AddBaseTypes(typeof(AbpUiResource)); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Docs.Localization; |
|||
|
|||
namespace Volo.Docs; |
|||
|
|||
public abstract class DocsControllerBase : AbpControllerBase |
|||
{ |
|||
protected DocsControllerBase() |
|||
{ |
|||
LocalizationResource = typeof(DocsResource); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using System.Threading.Tasks; |
|||
using Asp.Versioning; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Content; |
|||
using Volo.Docs.Common; |
|||
using Volo.Docs.Common.Documents; |
|||
|
|||
namespace Volo.Docs.Documents; |
|||
|
|||
[RemoteService(Name = DocsCommonRemoteServiceConsts.RemoteServiceName)] |
|||
[Area(DocsCommonRemoteServiceConsts.ModuleName)] |
|||
[ControllerName("Document")] |
|||
[Route("api/docs/documents")] |
|||
public class DocsDocumentPdfGeneratorController : DocsControllerBase, IDocumentPdfGeneratorAppService |
|||
{ |
|||
protected IDocumentPdfGeneratorAppService DocumentPdfGeneratorAppService { get; } |
|||
|
|||
public DocsDocumentPdfGeneratorController(IDocumentPdfGeneratorAppService documentPdfGeneratorAppService) |
|||
{ |
|||
DocumentPdfGeneratorAppService = documentPdfGeneratorAppService; |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("pdf")] |
|||
public Task<IRemoteStreamContent> GeneratePdfAsync(DocumentPdfGeneratorInput input) |
|||
{ |
|||
return DocumentPdfGeneratorAppService.GeneratePdfAsync(input); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System.Collections.Generic; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace Volo.Docs.Documents; |
|||
|
|||
public class DocumentParams |
|||
{ |
|||
[JsonPropertyName("Parameters")] |
|||
public List<DocumentParameter> Parameters { get; set; } = new(); |
|||
|
|||
|
|||
public class DocumentParameter |
|||
{ |
|||
[JsonPropertyName("name")] |
|||
public string Name { get; set; } |
|||
|
|||
[JsonPropertyName("displayName")] |
|||
public string DisplayName { get; set; } |
|||
|
|||
[JsonPropertyName("values")] |
|||
public List<Dictionary<string, string>> Values { get; set; } |
|||
} |
|||
} |
|||
@ -1,4 +1,4 @@ |
|||
namespace Volo.Docs.HtmlConverting |
|||
namespace Volo.Docs.Documents.Rendering |
|||
{ |
|||
public class DocumentPartialTemplateContent |
|||
{ |
|||
@ -0,0 +1,10 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Docs.Documents.Rendering; |
|||
|
|||
public class DocumentPartialTemplateWithValues |
|||
{ |
|||
public string Path { get; set; } |
|||
|
|||
public Dictionary<string, string> Parameters { get; set; } |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Docs.Documents.Rendering |
|||
{ |
|||
public class DocumentRenderParameters : Dictionary<string,string> |
|||
{ |
|||
public DocumentRenderParameters() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public DocumentRenderParameters(DocumentRenderParameters renderParameters) |
|||
{ |
|||
foreach (var parameter in renderParameters) |
|||
{ |
|||
Add(parameter.Key, parameter.Value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Docs.Documents.Rendering; |
|||
|
|||
public interface IDocumentSectionRenderer : ITransientDependency |
|||
{ |
|||
Task<string> RenderAsync(string document, DocumentRenderParameters parameters = null, List<DocumentPartialTemplateContent> partialTemplates = null); |
|||
|
|||
Task<Dictionary<string, List<string>>> GetAvailableParametersAsync(string document); |
|||
} |
|||
@ -0,0 +1,302 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Scriban; |
|||
using Volo.Abp; |
|||
using Volo.Extensions; |
|||
|
|||
namespace Volo.Docs.Documents.Rendering; |
|||
|
|||
public class ScribanDocumentSectionRenderer : IDocumentSectionRenderer |
|||
{ |
|||
protected readonly static List<DocsJsonSection> DocsJsonSections = |
|||
[ |
|||
new("````json", "````"), |
|||
new("```json", "```") |
|||
]; |
|||
|
|||
protected const string DocsParam = "//[doc-params]"; |
|||
protected const string DocsTemplates = "//[doc-template]"; |
|||
protected const string DocsNav = "//[doc-nav]"; |
|||
|
|||
public ILogger<ScribanDocumentSectionRenderer> Logger { get; set; } |
|||
|
|||
public ScribanDocumentSectionRenderer() |
|||
{ |
|||
Logger = NullLogger<ScribanDocumentSectionRenderer>.Instance; |
|||
} |
|||
|
|||
public async Task<string> RenderAsync( |
|||
string document, |
|||
DocumentRenderParameters parameters = null, |
|||
List<DocumentPartialTemplateContent> partialTemplates = null) |
|||
{ |
|||
if (partialTemplates != null && partialTemplates.Any()) |
|||
{ |
|||
document = SetPartialTemplates(document, partialTemplates); |
|||
} |
|||
|
|||
var scribanTemplate = Template.Parse(document); |
|||
|
|||
if (parameters == null) |
|||
{ |
|||
return await scribanTemplate.RenderAsync(); |
|||
} |
|||
|
|||
var result = await scribanTemplate.RenderAsync(parameters); |
|||
|
|||
return RemoveOptionsJson(result, DocsParam, DocsNav); |
|||
} |
|||
|
|||
public Task<Dictionary<string, List<string>>> GetAvailableParametersAsync(string document) |
|||
{ |
|||
return GetSectionAsync<Dictionary<string, List<string>>>(document, DocsParam); |
|||
} |
|||
|
|||
protected virtual async Task<T> GetSectionAsync<T>(string document, string sectionName) where T : new() |
|||
{ |
|||
try |
|||
{ |
|||
if (!HasJsonSection(document) || !document.Contains(sectionName)) |
|||
{ |
|||
return new T(); |
|||
} |
|||
|
|||
var (jsonBeginningIndex, jsonEndingIndex, insideJsonSection, _) = |
|||
GetJsonBeginEndIndexesAndPureJson(document, sectionName); |
|||
|
|||
if (jsonBeginningIndex < 0 || jsonEndingIndex <= 0 || string.IsNullOrWhiteSpace(insideJsonSection)) |
|||
{ |
|||
return new T(); |
|||
} |
|||
|
|||
var pureJson = insideJsonSection.Replace(sectionName, "").Trim(); |
|||
|
|||
if (!DocsJsonSerializerHelper.TryDeserialize<T>(pureJson, out var section)) |
|||
{ |
|||
throw new UserFriendlyException($"ERROR-20200327: Cannot validate JSON content for `{sectionName}`!"); |
|||
} |
|||
|
|||
return await Task.FromResult(section); |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
Logger.LogWarning("Unable to parse parameters of document."); |
|||
return new T(); |
|||
} |
|||
} |
|||
|
|||
protected static string RemoveOptionsJson(string document, params string[] sectionNames) |
|||
{ |
|||
foreach (var sectionName in sectionNames) |
|||
{ |
|||
var orgDocument = document; |
|||
|
|||
try |
|||
{ |
|||
if (!HasJsonSection(document) || !document.Contains(sectionName)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var (jsonBeginningIndex, jsonEndingIndex, insideJsonSection, jsonSection) = |
|||
GetJsonBeginEndIndexesAndPureJson(document, sectionName); |
|||
|
|||
if (jsonBeginningIndex < 0 || jsonEndingIndex <= 0 || string.IsNullOrWhiteSpace(insideJsonSection)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
document = document.Remove( |
|||
jsonBeginningIndex - jsonSection.Opener.Length, |
|||
(jsonEndingIndex + jsonSection.Closer.Length) - (jsonBeginningIndex - jsonSection.Opener.Length) |
|||
); |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
document = orgDocument; |
|||
} |
|||
} |
|||
|
|||
return document; |
|||
} |
|||
|
|||
protected static bool HasJsonSection(string document) |
|||
{ |
|||
return DocsJsonSections.Any(section => document.Contains(section.Opener) && document.Contains(section.Closer)); |
|||
} |
|||
|
|||
protected static (int, int, string, DocsJsonSection) GetJsonBeginEndIndexesAndPureJson(string document, string sectionName) |
|||
{ |
|||
foreach (var section in DocsJsonSections) |
|||
{ |
|||
var (jsonBeginningIndex, jsonEndingIndex, insideJsonSection) = |
|||
section.GetJsonBeginEndIndexesAndPureJson(document, sectionName); |
|||
|
|||
if (jsonBeginningIndex >= 0 && jsonEndingIndex > 0 && !string.IsNullOrWhiteSpace(insideJsonSection)) |
|||
{ |
|||
return (jsonBeginningIndex, jsonEndingIndex, insideJsonSection, section); |
|||
} |
|||
} |
|||
|
|||
return (-1, -1, "", null); |
|||
} |
|||
|
|||
protected static string SetPartialTemplates(string document, IReadOnlyCollection<DocumentPartialTemplateContent> templates) |
|||
{ |
|||
foreach (var section in DocsJsonSections) |
|||
{ |
|||
document = section.SetPartialTemplates(document, templates); |
|||
} |
|||
|
|||
return document; |
|||
} |
|||
|
|||
protected class DocsJsonSection |
|||
{ |
|||
public string Opener { get; } |
|||
public string Closer { get; } |
|||
|
|||
public DocsJsonSection(string opener, string closer) |
|||
{ |
|||
Opener = opener; |
|||
Closer = closer; |
|||
} |
|||
|
|||
public (int, int, string) GetJsonBeginEndIndexesAndPureJson(string document, string sectionName) |
|||
{ |
|||
var searchedIndex = 0; |
|||
|
|||
while (searchedIndex < document.Length) |
|||
{ |
|||
var jsonBeginningIndex = document.Substring(searchedIndex).IndexOf(Opener, StringComparison.Ordinal); |
|||
|
|||
if (jsonBeginningIndex < 0) |
|||
{ |
|||
return (-1, -1, ""); |
|||
} |
|||
|
|||
jsonBeginningIndex += Opener.Length + searchedIndex; |
|||
|
|||
var jsonEndingIndex = document.Substring(jsonBeginningIndex).IndexOf(Closer, StringComparison.Ordinal); |
|||
if (jsonEndingIndex < 0) |
|||
{ |
|||
return (-1, -1, ""); |
|||
} |
|||
|
|||
jsonEndingIndex += jsonBeginningIndex; |
|||
var insideJsonSection = document.Substring(jsonBeginningIndex, jsonEndingIndex - jsonBeginningIndex); |
|||
|
|||
if (insideJsonSection.IndexOf(sectionName, StringComparison.Ordinal) < 0) |
|||
{ |
|||
searchedIndex = jsonEndingIndex + Closer.Length; |
|||
continue; |
|||
} |
|||
|
|||
return (jsonBeginningIndex, jsonEndingIndex, insideJsonSection); |
|||
} |
|||
|
|||
return (-1, -1, ""); |
|||
} |
|||
|
|||
public async Task<List<DocumentPartialTemplateWithValues>> GetPartialTemplatesInDocumentAsync(string documentContent) |
|||
{ |
|||
var templates = new List<DocumentPartialTemplateWithValues>(); |
|||
|
|||
while (documentContent.Contains(Opener)) |
|||
{ |
|||
var afterJsonOpener = documentContent.Substring( |
|||
documentContent.IndexOf(Opener, StringComparison.Ordinal) + Opener.Length |
|||
); |
|||
|
|||
var betweenJsonOpenerAndCloser = afterJsonOpener.Substring(0, |
|||
afterJsonOpener.IndexOf(Closer, StringComparison.Ordinal) |
|||
); |
|||
|
|||
documentContent = afterJsonOpener.Substring( |
|||
afterJsonOpener.IndexOf(Closer, StringComparison.Ordinal) + Closer.Length |
|||
); |
|||
|
|||
if (!betweenJsonOpenerAndCloser.Contains(DocsTemplates)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var json = betweenJsonOpenerAndCloser.Substring( |
|||
betweenJsonOpenerAndCloser.IndexOf(DocsTemplates, StringComparison.Ordinal) + |
|||
DocsTemplates.Length); |
|||
|
|||
if (!DocsJsonSerializerHelper.TryDeserialize<DocumentPartialTemplateWithValues>(json, |
|||
out var template)) |
|||
{ |
|||
throw new UserFriendlyException( |
|||
$"ERROR-20200327: Cannot validate JSON content for `AvailableParameters`!"); |
|||
} |
|||
|
|||
templates.Add(template); |
|||
} |
|||
|
|||
return await Task.FromResult(templates); |
|||
} |
|||
|
|||
public string SetPartialTemplates(string document, IReadOnlyCollection<DocumentPartialTemplateContent> templates) |
|||
{ |
|||
var newDocument = new StringBuilder(); |
|||
|
|||
while (document.Contains(Opener)) |
|||
{ |
|||
var beforeJson = document.Substring(0, |
|||
document.IndexOf(Opener, StringComparison.Ordinal) + Opener.Length |
|||
); |
|||
|
|||
var afterJsonOpener = document.Substring( |
|||
document.IndexOf(Opener, StringComparison.Ordinal) + Opener.Length |
|||
); |
|||
|
|||
var betweenJsonOpenerAndCloser = afterJsonOpener.Substring(0, |
|||
afterJsonOpener.IndexOf(Closer, StringComparison.Ordinal) |
|||
); |
|||
|
|||
if (!betweenJsonOpenerAndCloser.Contains(DocsTemplates)) |
|||
{ |
|||
document = afterJsonOpener.Substring( |
|||
afterJsonOpener.IndexOf(Closer, StringComparison.Ordinal) + Closer.Length |
|||
); |
|||
|
|||
newDocument.Append(beforeJson + betweenJsonOpenerAndCloser + Closer); |
|||
continue; |
|||
} |
|||
|
|||
var json = betweenJsonOpenerAndCloser.Substring( |
|||
betweenJsonOpenerAndCloser.IndexOf(DocsTemplates, StringComparison.Ordinal) + |
|||
DocsTemplates.Length |
|||
); |
|||
|
|||
if (DocsJsonSerializerHelper.TryDeserialize<DocumentPartialTemplateWithValues>(json, |
|||
out var documentPartialTemplateWithValuesDto)) |
|||
{ |
|||
var template = |
|||
templates.FirstOrDefault(t => t.Path == documentPartialTemplateWithValuesDto.Path); |
|||
|
|||
var beforeTemplate = document.Substring(0, |
|||
document.IndexOf(Opener, StringComparison.Ordinal) |
|||
); |
|||
|
|||
newDocument.Append(beforeTemplate + template?.Content + Closer); |
|||
|
|||
document = afterJsonOpener.Substring( |
|||
afterJsonOpener.IndexOf(Closer, StringComparison.Ordinal) + Closer.Length |
|||
); |
|||
} |
|||
} |
|||
|
|||
newDocument.Append(document); |
|||
|
|||
return newDocument.ToString(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Docs.HtmlConverting |
|||
{ |
|||
public class DocumentPartialTemplateWithValuesDto |
|||
{ |
|||
public string Path { get; set; } |
|||
|
|||
public Dictionary<string, string> Parameters { get; set; } |
|||
} |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Docs.HtmlConverting |
|||
{ |
|||
public class DocumentRenderParameters : Dictionary<string,string> |
|||
{ |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Docs.HtmlConverting |
|||
{ |
|||
public interface IDocumentSectionRenderer: ITransientDependency |
|||
{ |
|||
Task<string> RenderAsync(string doucment, DocumentRenderParameters parameters = null, List<DocumentPartialTemplateContent> partialTemplates = null); |
|||
|
|||
Task<Dictionary<string, List<string>>> GetAvailableParametersAsync(string document); |
|||
|
|||
Task<List<DocumentPartialTemplateWithValuesDto>> GetPartialTemplatesInDocumentAsync(string documentContent); |
|||
|
|||
Task<DocumentNavigationsDto> GetDocumentNavigationsAsync(string documentContent); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Docs.Documents.Rendering; |
|||
|
|||
namespace Volo.Docs.HtmlConverting |
|||
{ |
|||
public interface IWebDocumentSectionRenderer: IDocumentSectionRenderer |
|||
{ |
|||
Task<List<DocumentPartialTemplateWithValues>> GetPartialTemplatesInDocumentAsync(string documentContent); |
|||
|
|||
Task<DocumentNavigationsDto> GetDocumentNavigationsAsync(string documentContent); |
|||
} |
|||
} |
|||
@ -1,316 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Scriban; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Volo.Abp; |
|||
using Volo.Extensions; |
|||
|
|||
namespace Volo.Docs.HtmlConverting |
|||
{ |
|||
public class ScribanDocumentSectionRenderer : IDocumentSectionRenderer |
|||
{ |
|||
private readonly static List<DocsJsonSection> DocsJsonSections = new List<DocsJsonSection> |
|||
{ |
|||
new DocsJsonSection("````json", "````"), |
|||
new DocsJsonSection("```json", "```") |
|||
}; |
|||
private const string DocsParam = "//[doc-params]"; |
|||
private const string DocsTemplates = "//[doc-template]"; |
|||
private const string DocsNav = "//[doc-nav]"; |
|||
|
|||
public ILogger<ScribanDocumentSectionRenderer> Logger { get; set; } |
|||
|
|||
public ScribanDocumentSectionRenderer() |
|||
{ |
|||
Logger = NullLogger<ScribanDocumentSectionRenderer>.Instance; |
|||
} |
|||
|
|||
public async Task<string> RenderAsync(string document, DocumentRenderParameters parameters = null, List<DocumentPartialTemplateContent> partialTemplates = null) |
|||
{ |
|||
if (partialTemplates != null && partialTemplates.Any()) |
|||
{ |
|||
document = SetPartialTemplates(document, partialTemplates); |
|||
} |
|||
|
|||
var scribanTemplate = Template.Parse(document); |
|||
|
|||
if (parameters == null) |
|||
{ |
|||
return await scribanTemplate.RenderAsync(); |
|||
} |
|||
|
|||
var result = await scribanTemplate.RenderAsync(parameters); |
|||
|
|||
return RemoveOptionsJson(result, DocsParam, DocsNav); |
|||
} |
|||
|
|||
public Task<Dictionary<string, List<string>>> GetAvailableParametersAsync(string document) |
|||
{ |
|||
return GetSectionAsync<Dictionary<string, List<string>>>(document, DocsParam); |
|||
} |
|||
|
|||
public Task<DocumentNavigationsDto> GetDocumentNavigationsAsync(string documentContent) |
|||
{ |
|||
return GetSectionAsync<DocumentNavigationsDto>(documentContent, DocsNav); |
|||
} |
|||
|
|||
protected virtual async Task<T> GetSectionAsync<T>(string document, string sectionName) where T : new() |
|||
{ |
|||
try |
|||
{ |
|||
if (!HasJsonSection(document) || !document.Contains(sectionName)) |
|||
{ |
|||
return new T(); |
|||
} |
|||
|
|||
var (jsonBeginningIndex, jsonEndingIndex, insideJsonSection, _) = GetJsonBeginEndIndexesAndPureJson(document, sectionName); |
|||
|
|||
if (jsonBeginningIndex < 0 || jsonEndingIndex <= 0 || string.IsNullOrWhiteSpace(insideJsonSection)) |
|||
{ |
|||
return new T(); |
|||
} |
|||
|
|||
var pureJson = insideJsonSection.Replace(sectionName, "").Trim(); |
|||
|
|||
if (!DocsJsonSerializerHelper.TryDeserialize<T>(pureJson, out var section)) |
|||
{ |
|||
throw new UserFriendlyException($"ERROR-20200327: Cannot validate JSON content for `{sectionName}`!"); |
|||
} |
|||
|
|||
return await Task.FromResult(section); |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
Logger.LogWarning("Unable to parse parameters of document."); |
|||
return new T(); |
|||
} |
|||
} |
|||
|
|||
private static string RemoveOptionsJson(string document, params string[] sectionNames) |
|||
{ |
|||
|
|||
foreach (var sectionName in sectionNames) |
|||
{ |
|||
var orgDocument = document; |
|||
|
|||
try |
|||
{ |
|||
if (!HasJsonSection(document) || !document.Contains(sectionName)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var (jsonBeginningIndex, jsonEndingIndex, insideJsonSection, jsonSection) = GetJsonBeginEndIndexesAndPureJson(document, sectionName); |
|||
|
|||
if (jsonBeginningIndex < 0 || jsonEndingIndex <= 0 || string.IsNullOrWhiteSpace(insideJsonSection)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
document = document.Remove( |
|||
jsonBeginningIndex - jsonSection.Opener.Length, |
|||
(jsonEndingIndex + jsonSection.Closer.Length) - (jsonBeginningIndex - jsonSection.Opener.Length) |
|||
); |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
document = orgDocument; |
|||
} |
|||
} |
|||
|
|||
return document; |
|||
} |
|||
|
|||
private static bool HasJsonSection(string document) |
|||
{ |
|||
return DocsJsonSections.Any(section => document.Contains(section.Opener) && document.Contains(section.Closer)); |
|||
} |
|||
|
|||
private static (int, int, string, DocsJsonSection) GetJsonBeginEndIndexesAndPureJson(string document, string sectionName) |
|||
{ |
|||
foreach (var section in DocsJsonSections) |
|||
{ |
|||
var (jsonBeginningIndex, jsonEndingIndex, insideJsonSection) = section.GetJsonBeginEndIndexesAndPureJson(document, sectionName); |
|||
|
|||
if (jsonBeginningIndex >= 0 && jsonEndingIndex > 0 && !string.IsNullOrWhiteSpace(insideJsonSection)) |
|||
{ |
|||
return (jsonBeginningIndex, jsonEndingIndex, insideJsonSection, section); |
|||
} |
|||
} |
|||
|
|||
return (-1, -1, "", null); |
|||
} |
|||
|
|||
public async Task<List<DocumentPartialTemplateWithValuesDto>> GetPartialTemplatesInDocumentAsync(string documentContent) |
|||
{ |
|||
var templates = new List<DocumentPartialTemplateWithValuesDto>(); |
|||
|
|||
foreach (var section in DocsJsonSections) |
|||
{ |
|||
templates.AddRange(await section.GetPartialTemplatesInDocumentAsync(documentContent)); |
|||
} |
|||
|
|||
return templates; |
|||
} |
|||
|
|||
private static string SetPartialTemplates(string document, IReadOnlyCollection<DocumentPartialTemplateContent> templates) |
|||
{ |
|||
foreach (var section in DocsJsonSections) |
|||
{ |
|||
document = section.SetPartialTemplates(document, templates); |
|||
} |
|||
|
|||
return document; |
|||
} |
|||
|
|||
private class DocsJsonSection |
|||
{ |
|||
public string Opener { get; } |
|||
public string Closer { get; } |
|||
|
|||
public DocsJsonSection(string opener, string closer) |
|||
{ |
|||
Opener = opener; |
|||
Closer = closer; |
|||
} |
|||
|
|||
public (int, int, string) GetJsonBeginEndIndexesAndPureJson(string document, string sectionName) |
|||
{ |
|||
var searchedIndex = 0; |
|||
|
|||
while (searchedIndex < document.Length) |
|||
{ |
|||
var jsonBeginningIndex = document.Substring(searchedIndex).IndexOf(Opener, StringComparison.Ordinal); |
|||
|
|||
if (jsonBeginningIndex < 0) |
|||
{ |
|||
return (-1, -1, ""); |
|||
} |
|||
|
|||
jsonBeginningIndex += Opener.Length + searchedIndex; |
|||
|
|||
var jsonEndingIndex = document.Substring(jsonBeginningIndex).IndexOf(Closer, StringComparison.Ordinal); |
|||
if (jsonEndingIndex < 0) |
|||
{ |
|||
return (-1, -1, ""); |
|||
} |
|||
|
|||
jsonEndingIndex += jsonBeginningIndex; |
|||
var insideJsonSection = document[jsonBeginningIndex..jsonEndingIndex]; |
|||
|
|||
if (insideJsonSection.IndexOf(sectionName, StringComparison.Ordinal) < 0) |
|||
{ |
|||
searchedIndex = jsonEndingIndex + Closer.Length; |
|||
continue; |
|||
} |
|||
|
|||
return (jsonBeginningIndex, jsonEndingIndex, insideJsonSection); |
|||
} |
|||
|
|||
return (-1, -1, ""); |
|||
} |
|||
|
|||
public async Task<List<DocumentPartialTemplateWithValuesDto>> GetPartialTemplatesInDocumentAsync( |
|||
string documentContent) |
|||
{ |
|||
var templates = new List<DocumentPartialTemplateWithValuesDto>(); |
|||
|
|||
while (documentContent.Contains(Opener)) |
|||
{ |
|||
var afterJsonOpener = documentContent.Substring( |
|||
documentContent.IndexOf(Opener, StringComparison.Ordinal) + Opener.Length |
|||
); |
|||
|
|||
var betweenJsonOpenerAndCloser = afterJsonOpener.Substring(0, |
|||
afterJsonOpener.IndexOf(Closer, StringComparison.Ordinal) |
|||
); |
|||
|
|||
documentContent = afterJsonOpener.Substring( |
|||
afterJsonOpener.IndexOf(Closer, StringComparison.Ordinal) + Closer.Length |
|||
); |
|||
|
|||
if (!betweenJsonOpenerAndCloser.Contains(DocsTemplates)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var json = betweenJsonOpenerAndCloser.Substring( |
|||
betweenJsonOpenerAndCloser.IndexOf(DocsTemplates, StringComparison.Ordinal) + |
|||
DocsTemplates.Length); |
|||
|
|||
if (!DocsJsonSerializerHelper.TryDeserialize<DocumentPartialTemplateWithValuesDto>(json, |
|||
out var template)) |
|||
{ |
|||
throw new UserFriendlyException( |
|||
$"ERROR-20200327: Cannot validate JSON content for `AvailableParameters`!"); |
|||
} |
|||
|
|||
templates.Add(template); |
|||
} |
|||
|
|||
return await Task.FromResult(templates); |
|||
} |
|||
|
|||
public string SetPartialTemplates(string document, |
|||
IReadOnlyCollection<DocumentPartialTemplateContent> templates) |
|||
{ |
|||
var newDocument = new StringBuilder(); |
|||
|
|||
while (document.Contains(Opener)) |
|||
{ |
|||
var beforeJson = document.Substring(0, |
|||
document.IndexOf(Opener, StringComparison.Ordinal) + Opener.Length |
|||
); |
|||
|
|||
var afterJsonOpener = document.Substring( |
|||
document.IndexOf(Opener, StringComparison.Ordinal) + Opener.Length |
|||
); |
|||
|
|||
var betweenJsonOpenerAndCloser = afterJsonOpener.Substring(0, |
|||
afterJsonOpener.IndexOf(Closer, StringComparison.Ordinal) |
|||
); |
|||
|
|||
if (!betweenJsonOpenerAndCloser.Contains(DocsTemplates)) |
|||
{ |
|||
document = afterJsonOpener.Substring( |
|||
afterJsonOpener.IndexOf(Closer, StringComparison.Ordinal) + Closer.Length |
|||
); |
|||
|
|||
newDocument.Append(beforeJson + betweenJsonOpenerAndCloser + Closer); |
|||
continue; |
|||
} |
|||
|
|||
var json = betweenJsonOpenerAndCloser.Substring( |
|||
betweenJsonOpenerAndCloser.IndexOf(DocsTemplates, StringComparison.Ordinal) + |
|||
DocsTemplates.Length |
|||
); |
|||
|
|||
if (DocsJsonSerializerHelper.TryDeserialize<DocumentPartialTemplateWithValuesDto>(json, |
|||
out var documentPartialTemplateWithValuesDto)) |
|||
{ |
|||
var template = |
|||
templates.FirstOrDefault(t => t.Path == documentPartialTemplateWithValuesDto.Path); |
|||
|
|||
var beforeTemplate = document.Substring(0, |
|||
document.IndexOf(Opener, StringComparison.Ordinal) |
|||
); |
|||
|
|||
newDocument.Append(beforeTemplate + template?.Content + Closer); |
|||
|
|||
document = afterJsonOpener.Substring( |
|||
afterJsonOpener.IndexOf(Closer, StringComparison.Ordinal) + Closer.Length |
|||
); |
|||
} |
|||
} |
|||
|
|||
newDocument.Append(document); |
|||
|
|||
return newDocument.ToString(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Scriban; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Volo.Abp; |
|||
using Volo.Abp.ObjectMapping; |
|||
using Volo.Docs.Documents.Rendering; |
|||
using Volo.Extensions; |
|||
|
|||
namespace Volo.Docs.HtmlConverting |
|||
{ |
|||
public class ScribanWebDocumentSectionRenderer : ScribanDocumentSectionRenderer, IWebDocumentSectionRenderer |
|||
{ |
|||
private IObjectMapper ObjectMapper { get; set; } |
|||
|
|||
public Task<DocumentNavigationsDto> GetDocumentNavigationsAsync(string documentContent) |
|||
{ |
|||
return GetSectionAsync<DocumentNavigationsDto>(documentContent, DocsNav); |
|||
} |
|||
|
|||
public async Task<List<DocumentPartialTemplateWithValues>> GetPartialTemplatesInDocumentAsync( |
|||
string documentContent) |
|||
{ |
|||
var templates = new List<DocumentPartialTemplateWithValues>(); |
|||
|
|||
foreach (var section in DocsJsonSections) |
|||
{ |
|||
templates.AddRange(await section.GetPartialTemplatesInDocumentAsync(documentContent)); |
|||
} |
|||
|
|||
return templates; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue