diff --git a/Directory.Packages.props b/Directory.Packages.props index a8ec5e695c..59aa37e3fe 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,6 +38,9 @@ + + + @@ -183,4 +186,4 @@ - + \ No newline at end of file diff --git a/modules/docs/Volo.Docs.sln b/modules/docs/Volo.Docs.sln index 77c5ae1761..af9282dca3 100644 --- a/modules/docs/Volo.Docs.sln +++ b/modules/docs/Volo.Docs.sln @@ -63,11 +63,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Docs.MongoDB.Tests", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Docs.Installer", "src\Volo.Docs.Installer\Volo.Docs.Installer.csproj", "{50B9AC1D-C03E-47AA-9ED8-E7986BCFABA1}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{AA7E58C3-E223-455F-81BD-4D2E2E152624}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Docs.Common.Application", "src\Volo.Docs.Common.Application\Volo.Docs.Common.Application.csproj", "{914EDEC4-EFCE-46D5-B6E0-A928B1907307}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Docs.Common.Application.Contracts", "src\Volo.Docs.Common.Application.Contracts\Volo.Docs.Common.Application.Contracts.csproj", "{D75944C2-FAB0-41A8-AB2A-1A2F97849302}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Docs.Common.Application.Contracts", "src\Volo.Docs.Common.Application.Contracts\Volo.Docs.Common.Application.Contracts.csproj", "{8CA19527-6870-4617-B6D1-EA8CAF224DF4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Docs.Common.Application", "src\Volo.Docs.Common.Application\Volo.Docs.Common.Application.csproj", "{A96F4615-8405-493D-AA06-727D6F29B5F5}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{F778B2C3-2E6D-4CF3-ACA7-10E21E6A7961}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Docs.Common.HttpApi", "src\Volo.Docs.Common.HttpApi\Volo.Docs.Common.HttpApi.csproj", "{052C076D-56FF-4DFA-838B-48BD30A1DB0A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Docs.Common.HttpApi.Client", "src\Volo.Docs.Common.HttpApi.Client\Volo.Docs.Common.HttpApi.Client.csproj", "{349C36A0-552D-44A3-AC74-03A4828D43AB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -171,14 +175,22 @@ Global {50B9AC1D-C03E-47AA-9ED8-E7986BCFABA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {50B9AC1D-C03E-47AA-9ED8-E7986BCFABA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {50B9AC1D-C03E-47AA-9ED8-E7986BCFABA1}.Release|Any CPU.Build.0 = Release|Any CPU - {D75944C2-FAB0-41A8-AB2A-1A2F97849302}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D75944C2-FAB0-41A8-AB2A-1A2F97849302}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D75944C2-FAB0-41A8-AB2A-1A2F97849302}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D75944C2-FAB0-41A8-AB2A-1A2F97849302}.Release|Any CPU.Build.0 = Release|Any CPU - {A96F4615-8405-493D-AA06-727D6F29B5F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A96F4615-8405-493D-AA06-727D6F29B5F5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A96F4615-8405-493D-AA06-727D6F29B5F5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A96F4615-8405-493D-AA06-727D6F29B5F5}.Release|Any CPU.Build.0 = Release|Any CPU + {914EDEC4-EFCE-46D5-B6E0-A928B1907307}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {914EDEC4-EFCE-46D5-B6E0-A928B1907307}.Debug|Any CPU.Build.0 = Debug|Any CPU + {914EDEC4-EFCE-46D5-B6E0-A928B1907307}.Release|Any CPU.ActiveCfg = Release|Any CPU + {914EDEC4-EFCE-46D5-B6E0-A928B1907307}.Release|Any CPU.Build.0 = Release|Any CPU + {8CA19527-6870-4617-B6D1-EA8CAF224DF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CA19527-6870-4617-B6D1-EA8CAF224DF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CA19527-6870-4617-B6D1-EA8CAF224DF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CA19527-6870-4617-B6D1-EA8CAF224DF4}.Release|Any CPU.Build.0 = Release|Any CPU + {052C076D-56FF-4DFA-838B-48BD30A1DB0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {052C076D-56FF-4DFA-838B-48BD30A1DB0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {052C076D-56FF-4DFA-838B-48BD30A1DB0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {052C076D-56FF-4DFA-838B-48BD30A1DB0A}.Release|Any CPU.Build.0 = Release|Any CPU + {349C36A0-552D-44A3-AC74-03A4828D43AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {349C36A0-552D-44A3-AC74-03A4828D43AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {349C36A0-552D-44A3-AC74-03A4828D43AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {349C36A0-552D-44A3-AC74-03A4828D43AB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -211,9 +223,11 @@ Global {DBE846CD-1BED-4F2C-ABF2-94F6240BCB9B} = {A982A58E-1E92-4764-9F56-39E7AABB8556} {C5E2A2A3-D54D-4C2E-97BA-EA50A49ED7AD} = {59D430A9-AC61-4457-8338-5DA0705ABB5D} {50B9AC1D-C03E-47AA-9ED8-E7986BCFABA1} = {A982A58E-1E92-4764-9F56-39E7AABB8556} - {AA7E58C3-E223-455F-81BD-4D2E2E152624} = {42416152-5BAB-4706-93A6-57A19E71FE14} - {D75944C2-FAB0-41A8-AB2A-1A2F97849302} = {AA7E58C3-E223-455F-81BD-4D2E2E152624} - {A96F4615-8405-493D-AA06-727D6F29B5F5} = {AA7E58C3-E223-455F-81BD-4D2E2E152624} + {F778B2C3-2E6D-4CF3-ACA7-10E21E6A7961} = {42416152-5BAB-4706-93A6-57A19E71FE14} + {914EDEC4-EFCE-46D5-B6E0-A928B1907307} = {F778B2C3-2E6D-4CF3-ACA7-10E21E6A7961} + {8CA19527-6870-4617-B6D1-EA8CAF224DF4} = {F778B2C3-2E6D-4CF3-ACA7-10E21E6A7961} + {052C076D-56FF-4DFA-838B-48BD30A1DB0A} = {F778B2C3-2E6D-4CF3-ACA7-10E21E6A7961} + {349C36A0-552D-44A3-AC74-03A4828D43AB} = {F778B2C3-2E6D-4CF3-ACA7-10E21E6A7961} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {13691265-2547-4FFF-B757-E8FACB05679D} diff --git a/modules/docs/app/VoloDocs.Web/VoloDocsWebModule.cs b/modules/docs/app/VoloDocs.Web/VoloDocsWebModule.cs index 743944c6d3..cf864f1e50 100644 --- a/modules/docs/app/VoloDocs.Web/VoloDocsWebModule.cs +++ b/modules/docs/app/VoloDocs.Web/VoloDocsWebModule.cs @@ -36,6 +36,7 @@ using Volo.Abp.PermissionManagement.HttpApi; using Volo.Abp.Validation.Localization; using Volo.Docs.Documents.FullSearch.Elastic; using Volo.Abp.Caching.StackExchangeRedis; +using Volo.Docs.Common.Documents; namespace VoloDocs.Web { @@ -170,6 +171,11 @@ namespace VoloDocs.Web options.EnableGoogleProgrammableSearchEngine = true; options.GoogleSearchEngineId = "77c7266532da1427f"; }); + + Configure(options => + { + options.BaseUrl = configuration["App:selfUrl"]; + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo.Docs.Admin.Application.Contracts.csproj b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo.Docs.Admin.Application.Contracts.csproj index a49d9146db..796a4a83c4 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo.Docs.Admin.Application.Contracts.csproj +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo.Docs.Admin.Application.Contracts.csproj @@ -18,6 +18,7 @@ + diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/DocsAdminApplicationContractsModule.cs b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/DocsAdminApplicationContractsModule.cs index 0962a28aef..1e20041974 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/DocsAdminApplicationContractsModule.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/DocsAdminApplicationContractsModule.cs @@ -4,6 +4,7 @@ using Volo.Abp.Authorization.Permissions; using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.VirtualFileSystem; +using Volo.Docs.Common; using Volo.Docs.Localization; namespace Volo.Docs.Admin @@ -11,7 +12,8 @@ namespace Volo.Docs.Admin [DependsOn( typeof(DocsDomainSharedModule), typeof(AbpDddApplicationContractsModule), - typeof(AbpAuthorizationAbstractionsModule) + typeof(AbpAuthorizationAbstractionsModule), + typeof(DocsCommonApplicationContractsModule) )] public class DocsAdminApplicationContractsModule : AbpModule { diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.csproj b/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.csproj index 3d947ead5e..7279eeee67 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.csproj +++ b/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.csproj @@ -12,6 +12,7 @@ + diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationModule.cs b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationModule.cs index 0b78c53973..7e6983d65e 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationModule.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationModule.cs @@ -3,12 +3,14 @@ using Volo.Abp.Application; using Volo.Abp.AutoMapper; using Volo.Abp.Caching; using Volo.Abp.Modularity; +using Volo.Docs.Common; namespace Volo.Docs.Admin { [DependsOn( typeof(DocsDomainModule), typeof(DocsAdminApplicationContractsModule), + typeof(DocsCommonApplicationModule), typeof(AbpCachingModule), typeof(AbpAutoMapperModule), typeof(AbpDddApplicationModule) diff --git a/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo.Docs.Admin.HttpApi.csproj b/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo.Docs.Admin.HttpApi.csproj index 6d718f03bb..25a0be0294 100644 --- a/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo.Docs.Admin.HttpApi.csproj +++ b/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo.Docs.Admin.HttpApi.csproj @@ -12,6 +12,7 @@ + diff --git a/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo/Docs/Admin/DocsAdminHttpApiModule.cs b/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo/Docs/Admin/DocsAdminHttpApiModule.cs index ed62f35e6f..ba7f1f7fbb 100644 --- a/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo/Docs/Admin/DocsAdminHttpApiModule.cs +++ b/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo/Docs/Admin/DocsAdminHttpApiModule.cs @@ -4,12 +4,14 @@ using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Docs.Localization; using Microsoft.Extensions.DependencyInjection; +using Volo.Docs.Common; namespace Volo.Docs.Admin { [DependsOn( typeof(DocsAdminApplicationContractsModule), - typeof(AbpAspNetCoreMvcModule) + typeof(AbpAspNetCoreMvcModule), + typeof(DocsCommonHttpApiModule) )] public class DocsAdminHttpApiModule : AbpModule { diff --git a/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/DocsCommonPermissionDefinitionProvider.cs b/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/DocsCommonPermissionDefinitionProvider.cs index 5f59a60548..16f3597152 100644 --- a/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/DocsCommonPermissionDefinitionProvider.cs +++ b/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/DocsCommonPermissionDefinitionProvider.cs @@ -10,7 +10,7 @@ namespace Volo.Docs.Common { var group = context.AddGroup(DocsCommonPermissions.GroupName, L("Permission:DocumentManagement.Common")); - group.AddPermission(DocsCommonPermissions.Documents.PdfCreation, L("Permission:PdfCreation")); + group.AddPermission(DocsCommonPermissions.Documents.PdfGeneration, L("Permission:PdfGeneration")); } private static LocalizableString L(string name) diff --git a/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/DocsCommonPermissions.cs b/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/DocsCommonPermissions.cs index c0aae9275a..4d2d2a3827 100644 --- a/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/DocsCommonPermissions.cs +++ b/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/DocsCommonPermissions.cs @@ -8,7 +8,7 @@ namespace Volo.Docs.Common public static class Documents { - public const string PdfCreation = GroupName + ".PdfCreation"; + public const string PdfGeneration = GroupName + ".PdfGeneration"; } public static string[] GetAll() diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.csproj b/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.csproj index d87c22ab09..fa4173f985 100644 --- a/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.csproj +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.csproj @@ -17,6 +17,9 @@ + + + diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsDocumentPdfContainer.cs b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocsDocumentPdfContainer.cs similarity index 74% rename from modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsDocumentPdfContainer.cs rename to modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocsDocumentPdfContainer.cs index e8b7f3c3fb..63e3652a3b 100644 --- a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsDocumentPdfContainer.cs +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocsDocumentPdfContainer.cs @@ -1,6 +1,6 @@ using Volo.Abp.BlobStoring; -namespace Volo.Docs.Common; +namespace Volo.Docs.Common.Documents; [BlobContainerName("docs-document-pdf")] public class DocsDocumentPdfContainer diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocsDocumentPdfGeneratorOptions.cs b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocsDocumentPdfGeneratorOptions.cs new file mode 100644 index 0000000000..6d2ff2bc82 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocsDocumentPdfGeneratorOptions.cs @@ -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 = @" + + + + + + + {{content-placeholder}} + + "; + + 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; + }"; + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocumentPdfGenerator.cs b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocumentPdfGenerator.cs new file mode 100644 index 0000000000..f54eafe116 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocumentPdfGenerator.cs @@ -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 Options { get; } + protected ILogger Logger { get; set; } + protected IDocumentSource DocumentSource { get; set; } + protected DocumentParams DocumentParams { get; set; } + protected List AllPdfDocumentNodes { get; set; } = new(); + protected MarkdownPipeline MarkdownPipeline { get; } = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); + + public DocumentPdfGenerator( + IDocumentSourceFactory documentStoreFactory, + IDocumentRepository documentRepository, + IOptions options, + IDocumentSectionRenderer documentSectionRenderer) + { + DocumentStoreFactory = documentStoreFactory; + DocumentRepository = documentRepository; + Options = options; + DocumentSectionRenderer = documentSectionRenderer; + Logger = NullLogger.Instance; + } + + public virtual async Task 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 ConvertDocumentsToHtmlAsync(List 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 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 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 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 navigations, + Project project, + string version, + string languageCode, + PdfDocumentNode parentPdfDocumentNode = null) + { + var parameterCombinationsDocuments = new List(); + 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 GetNavigationAsync( + Project project, + string version, + string languageCode) + { + var navigationDocument = await GetDocumentAsync(project, project.NavigationDocumentName, version, languageCode); + + if (!DocsJsonSerializerHelper.TryDeserialize(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 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(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 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 GenerateAllParameterCombinations(List parameterKeys, Dictionary> parameters) + { + var parameterCombinations = new List(); + + 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 = $"
{htmlContent}
"; + + htmlContent = Regex.Replace(htmlContent, @"(]*)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 Children { get; set; } = []; + public DocumentRenderParameters RenderParameters { get; set; } + public bool HasChildren => Children.Any(); + public bool IgnoreOnOutline { get; set; } + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocumentPdfGeneratorAppService.cs b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocumentPdfGeneratorAppService.cs index 860f647a4f..cc752d1492 100644 --- a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocumentPdfGeneratorAppService.cs +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/DocumentPdfGeneratorAppService.cs @@ -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 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 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 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 GetNavigationAsync( - IDocumentSource documentStore, - Project project, - string languageCode, - string version) + public async Task GeneratePdfAsync(DocumentPdfGeneratorInput input) { - var navigationDocument = await GetDocumentAsync(documentStore, project, project.NavigationDocumentName, languageCode, version); - - if (!DocsJsonSerializerHelper.TryDeserialize(navigationDocument.Content, out var navigation)) - { - throw new UserFriendlyException($"Cannot validate navigation file '{project.NavigationDocumentName}' for the project {project.Name}."); - } - - return navigation; - } - - private async Task 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); } } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/HtmlIdTagWorkerFactory.cs b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/HtmlIdTagWorkerFactory.cs new file mode 100644 index 0000000000..02078486e2 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/HtmlIdTagWorkerFactory.cs @@ -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 _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()); + } + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/IDocumentPdfGenerator.cs b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/IDocumentPdfGenerator.cs new file mode 100644 index 0000000000..3fced15bf8 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Documents/IDocumentPdfGenerator.cs @@ -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 GenerateAsync(Project project, string version, string languageCode); +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Common.HttpApi.Client/Volo.Docs.Common.HttpApi.Client.csproj b/modules/docs/src/Volo.Docs.Common.HttpApi.Client/Volo.Docs.Common.HttpApi.Client.csproj new file mode 100644 index 0000000000..ff4c3301fc --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.HttpApi.Client/Volo.Docs.Common.HttpApi.Client.csproj @@ -0,0 +1,19 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0 + Volo.Docs.Common.HttpApi.Client + Volo.Docs.Common.HttpApi.Client + + + + + + + + + + diff --git a/modules/docs/src/Volo.Docs.Common.HttpApi/Volo.Docs.Common.HttpApi.csproj b/modules/docs/src/Volo.Docs.Common.HttpApi/Volo.Docs.Common.HttpApi.csproj new file mode 100644 index 0000000000..001778b838 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.HttpApi/Volo.Docs.Common.HttpApi.csproj @@ -0,0 +1,18 @@ + + + + + + + net9.0 + Volo.Docs.Common.HttpApi + Volo.Docs.Common.HttpApi + + + + + + + + + diff --git a/modules/docs/src/Volo.Docs.Common.HttpApi/Volo/Docs/DocsCommonHttpApiModule.cs b/modules/docs/src/Volo.Docs.Common.HttpApi/Volo/Docs/DocsCommonHttpApiModule.cs new file mode 100644 index 0000000000..09ff45507b --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.HttpApi/Volo/Docs/DocsCommonHttpApiModule.cs @@ -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(mvcBuilder => + { + mvcBuilder.AddApplicationPartIfNotExists(typeof(DocsCommonHttpApiModule).Assembly); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Resources + .Get() + .AddBaseTypes(typeof(AbpUiResource)); + }); + } + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Common.HttpApi/Volo/Docs/DocsControllerBase.cs b/modules/docs/src/Volo.Docs.Common.HttpApi/Volo/Docs/DocsControllerBase.cs new file mode 100644 index 0000000000..8d730acd4f --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.HttpApi/Volo/Docs/DocsControllerBase.cs @@ -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); + } +} diff --git a/modules/docs/src/Volo.Docs.Common.HttpApi/Volo/Docs/Documents/DocsDocumentPdfGeneratorController.cs b/modules/docs/src/Volo.Docs.Common.HttpApi/Volo/Docs/Documents/DocsDocumentPdfGeneratorController.cs new file mode 100644 index 0000000000..bfd5be9224 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.HttpApi/Volo/Docs/Documents/DocsDocumentPdfGeneratorController.cs @@ -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 GeneratePdfAsync(DocumentPdfGeneratorInput input) + { + return DocumentPdfGeneratorAppService.GeneratePdfAsync(input); + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain.Shared/Volo.Docs.Domain.Shared.csproj b/modules/docs/src/Volo.Docs.Domain.Shared/Volo.Docs.Domain.Shared.csproj index 8a4911b9b9..2ce827d009 100644 --- a/modules/docs/src/Volo.Docs.Domain.Shared/Volo.Docs.Domain.Shared.csproj +++ b/modules/docs/src/Volo.Docs.Domain.Shared/Volo.Docs.Domain.Shared.csproj @@ -11,6 +11,7 @@ + diff --git a/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/DocumentParams.cs b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/DocumentParams.cs new file mode 100644 index 0000000000..ae116b850f --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/DocumentParams.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Volo.Docs.Documents; + +public class DocumentParams +{ + [JsonPropertyName("Parameters")] + public List 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> Values { get; set; } + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/HtmlConverting/DocumentPartialTemplateContent.cs b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/DocumentPartialTemplateContent.cs similarity index 77% rename from modules/docs/src/Volo.Docs.Web/HtmlConverting/DocumentPartialTemplateContent.cs rename to modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/DocumentPartialTemplateContent.cs index 5db9fc8164..66a5d27a16 100644 --- a/modules/docs/src/Volo.Docs.Web/HtmlConverting/DocumentPartialTemplateContent.cs +++ b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/DocumentPartialTemplateContent.cs @@ -1,4 +1,4 @@ -namespace Volo.Docs.HtmlConverting +namespace Volo.Docs.Documents.Rendering { public class DocumentPartialTemplateContent { diff --git a/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/DocumentPartialTemplateWithValues.cs b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/DocumentPartialTemplateWithValues.cs new file mode 100644 index 0000000000..4ae900256e --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/DocumentPartialTemplateWithValues.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Volo.Docs.Documents.Rendering; + +public class DocumentPartialTemplateWithValues +{ + public string Path { get; set; } + + public Dictionary Parameters { get; set; } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/DocumentRenderParameters.cs b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/DocumentRenderParameters.cs new file mode 100644 index 0000000000..dc7bf95e62 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/DocumentRenderParameters.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Volo.Docs.Documents.Rendering +{ + public class DocumentRenderParameters : Dictionary + { + public DocumentRenderParameters() + { + + } + + public DocumentRenderParameters(DocumentRenderParameters renderParameters) + { + foreach (var parameter in renderParameters) + { + Add(parameter.Key, parameter.Value); + } + } + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/IDocumentSectionRenderer.cs b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/IDocumentSectionRenderer.cs new file mode 100644 index 0000000000..9179601545 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/IDocumentSectionRenderer.cs @@ -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 RenderAsync(string document, DocumentRenderParameters parameters = null, List partialTemplates = null); + + Task>> GetAvailableParametersAsync(string document); +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/ScribanDocumentSectionRenderer.cs b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/ScribanDocumentSectionRenderer.cs new file mode 100644 index 0000000000..02881c5a67 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/Rendering/ScribanDocumentSectionRenderer.cs @@ -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 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 Logger { get; set; } + + public ScribanDocumentSectionRenderer() + { + Logger = NullLogger.Instance; + } + + public async Task RenderAsync( + string document, + DocumentRenderParameters parameters = null, + List 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>> GetAvailableParametersAsync(string document) + { + return GetSectionAsync>>(document, DocsParam); + } + + protected virtual async Task GetSectionAsync(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(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 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> GetPartialTemplatesInDocumentAsync(string documentContent) + { + var templates = new List(); + + 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(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 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(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(); + } + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/DocsWebAutoMapperProfile.cs b/modules/docs/src/Volo.Docs.Web/DocsWebAutoMapperProfile.cs index a4c4c7647e..e0a383e691 100644 --- a/modules/docs/src/Volo.Docs.Web/DocsWebAutoMapperProfile.cs +++ b/modules/docs/src/Volo.Docs.Web/DocsWebAutoMapperProfile.cs @@ -1,6 +1,4 @@ using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.Docs.Documents; namespace Volo.Docs { @@ -8,7 +6,6 @@ namespace Volo.Docs { public DocsWebAutoMapperProfile() { - } } } diff --git a/modules/docs/src/Volo.Docs.Web/HtmlConverting/DocumentPartialTemplateWithValuesDto.cs b/modules/docs/src/Volo.Docs.Web/HtmlConverting/DocumentPartialTemplateWithValuesDto.cs deleted file mode 100644 index d3ea14921c..0000000000 --- a/modules/docs/src/Volo.Docs.Web/HtmlConverting/DocumentPartialTemplateWithValuesDto.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace Volo.Docs.HtmlConverting -{ - public class DocumentPartialTemplateWithValuesDto - { - public string Path { get; set; } - - public Dictionary Parameters { get; set; } - } -} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/HtmlConverting/DocumentRenderParameters.cs b/modules/docs/src/Volo.Docs.Web/HtmlConverting/DocumentRenderParameters.cs deleted file mode 100644 index 7d9f26832f..0000000000 --- a/modules/docs/src/Volo.Docs.Web/HtmlConverting/DocumentRenderParameters.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections.Generic; - -namespace Volo.Docs.HtmlConverting -{ - public class DocumentRenderParameters : Dictionary - { - } -} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/HtmlConverting/IDocumentSectionRenderer.cs b/modules/docs/src/Volo.Docs.Web/HtmlConverting/IDocumentSectionRenderer.cs deleted file mode 100644 index 73560a7273..0000000000 --- a/modules/docs/src/Volo.Docs.Web/HtmlConverting/IDocumentSectionRenderer.cs +++ /dev/null @@ -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 RenderAsync(string doucment, DocumentRenderParameters parameters = null, List partialTemplates = null); - - Task>> GetAvailableParametersAsync(string document); - - Task> GetPartialTemplatesInDocumentAsync(string documentContent); - - Task GetDocumentNavigationsAsync(string documentContent); - } -} diff --git a/modules/docs/src/Volo.Docs.Web/HtmlConverting/IWebDocumentSectionRenderer.cs b/modules/docs/src/Volo.Docs.Web/HtmlConverting/IWebDocumentSectionRenderer.cs new file mode 100644 index 0000000000..b485c7b132 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Web/HtmlConverting/IWebDocumentSectionRenderer.cs @@ -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> GetPartialTemplatesInDocumentAsync(string documentContent); + + Task GetDocumentNavigationsAsync(string documentContent); + } +} diff --git a/modules/docs/src/Volo.Docs.Web/HtmlConverting/ScribanDocumentSectionRenderer.cs b/modules/docs/src/Volo.Docs.Web/HtmlConverting/ScribanDocumentSectionRenderer.cs deleted file mode 100644 index e8d0149cf8..0000000000 --- a/modules/docs/src/Volo.Docs.Web/HtmlConverting/ScribanDocumentSectionRenderer.cs +++ /dev/null @@ -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 DocsJsonSections = new List - { - 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 Logger { get; set; } - - public ScribanDocumentSectionRenderer() - { - Logger = NullLogger.Instance; - } - - public async Task RenderAsync(string document, DocumentRenderParameters parameters = null, List 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>> GetAvailableParametersAsync(string document) - { - return GetSectionAsync>>(document, DocsParam); - } - - public Task GetDocumentNavigationsAsync(string documentContent) - { - return GetSectionAsync(documentContent, DocsNav); - } - - protected virtual async Task GetSectionAsync(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(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> GetPartialTemplatesInDocumentAsync(string documentContent) - { - var templates = new List(); - - foreach (var section in DocsJsonSections) - { - templates.AddRange(await section.GetPartialTemplatesInDocumentAsync(documentContent)); - } - - return templates; - } - - private static string SetPartialTemplates(string document, IReadOnlyCollection 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> GetPartialTemplatesInDocumentAsync( - string documentContent) - { - var templates = new List(); - - 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(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 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(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(); - } - } - } -} diff --git a/modules/docs/src/Volo.Docs.Web/HtmlConverting/ScribanWebDocumentSectionRenderer.cs b/modules/docs/src/Volo.Docs.Web/HtmlConverting/ScribanWebDocumentSectionRenderer.cs new file mode 100644 index 0000000000..e4a26b2df6 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Web/HtmlConverting/ScribanWebDocumentSectionRenderer.cs @@ -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 GetDocumentNavigationsAsync(string documentContent) + { + return GetSectionAsync(documentContent, DocsNav); + } + + public async Task> GetPartialTemplatesInDocumentAsync( + string documentContent) + { + var templates = new List(); + + foreach (var section in DocsJsonSections) + { + templates.AddRange(await section.GetPartialTemplatesInDocumentAsync(documentContent)); + } + + return templates; + } + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs index 2bb7c82c21..c27e0d1f6f 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs @@ -19,6 +19,7 @@ using Volo.Abp.Data; using Volo.Abp.Domain.Entities; using Volo.Abp.EventBus.Local; using Volo.Docs.Documents; +using Volo.Docs.Documents.Rendering; using Volo.Docs.HtmlConverting; using Volo.Docs.Models; using Volo.Docs.Projects; @@ -96,7 +97,7 @@ namespace Volo.Docs.Pages.Documents.Project private readonly IDocumentAppService _documentAppService; private readonly IDocumentToHtmlConverterFactory _documentToHtmlConverterFactory; private readonly IProjectAppService _projectAppService; - private readonly IDocumentSectionRenderer _documentSectionRenderer; + private readonly IWebDocumentSectionRenderer _webDocumentSectionRenderer; private readonly DocsUiOptions _uiOptions; protected IDocsLinkGenerator DocsLinkGenerator => LazyServiceProvider.LazyGetRequiredService(); @@ -108,17 +109,16 @@ namespace Volo.Docs.Pages.Documents.Project IDocumentToHtmlConverterFactory documentToHtmlConverterFactory, IProjectAppService projectAppService, IOptions options, - IDocumentSectionRenderer documentSectionRenderer) + IWebDocumentSectionRenderer webDocumentSectionRenderer) { ObjectMapperContext = typeof(DocsWebModule); _documentAppService = documentAppService; _documentToHtmlConverterFactory = documentToHtmlConverterFactory; _projectAppService = projectAppService; - _documentSectionRenderer = documentSectionRenderer; + _webDocumentSectionRenderer = webDocumentSectionRenderer; _uiOptions = options.Value; - - + LocalizationResourceType = typeof(DocsResource); } @@ -561,11 +561,11 @@ namespace Volo.Docs.Pages.Documents.Project var partialTemplates = await GetDocumentPartialTemplatesAsync(); - DocumentNavigationsDto = await _documentSectionRenderer.GetDocumentNavigationsAsync(Document.Content); + DocumentNavigationsDto = await _webDocumentSectionRenderer.GetDocumentNavigationsAsync(Document.Content); try { - Document.Content = await _documentSectionRenderer.RenderAsync(Document.Content, UserPreferences, partialTemplates); + Document.Content = await _webDocumentSectionRenderer.RenderAsync(Document.Content, UserPreferences, partialTemplates); } catch (Exception e) { @@ -619,7 +619,7 @@ namespace Volo.Docs.Pages.Documents.Project private async Task> GetDocumentPartialTemplatesAsync() { - var partialTemplatesInDocument = await _documentSectionRenderer.GetPartialTemplatesInDocumentAsync(Document.Content); + var partialTemplatesInDocument = await _webDocumentSectionRenderer.GetPartialTemplatesInDocumentAsync(Document.Content); if (!partialTemplatesInDocument?.Any(t => t.Parameters != null) ?? true) { @@ -772,7 +772,9 @@ namespace Volo.Docs.Pages.Documents.Project return; } - var availableParameters = await _documentSectionRenderer.GetAvailableParametersAsync(Document.Content); + var availableParameters = await _webDocumentSectionRenderer.GetAvailableParametersAsync(Document.Content); + var parameterCombinations = new List>(); + GenerateCombinations(0, availableParameters.Keys.ToList(),availableParameters,new Dictionary(), parameterCombinations); DocumentPreferences = new DocumentParametersDto { @@ -813,11 +815,32 @@ namespace Volo.Docs.Pages.Documents.Project { if (!DocumentPreferences?.Parameters?.Any() ?? true) { - return; + return; } AlternativeOptionLinkQueries = CollectAlternativeOptionLinksRecursively(); } + + void GenerateCombinations( + int keyIndex, + List parameterKeys , + Dictionary> parameters, + Dictionary currentCombination, + List> parameterCombinations ) + { + if (keyIndex == parameterKeys.Count) + { + parameterCombinations.Add(new Dictionary(currentCombination)); + return; + } + + var currentKey = parameterKeys[keyIndex]; + foreach (var value in parameters[currentKey]) + { + currentCombination[currentKey] = value; + GenerateCombinations(keyIndex + 1,parameterKeys, parameters, currentCombination,parameterCombinations); + } + } private List CollectAlternativeOptionLinksRecursively(int index = 0) { diff --git a/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj b/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj index 1d6e0e1ace..2c36600107 100644 --- a/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj +++ b/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj @@ -23,7 +23,7 @@ - +