Browse Source

Merge pull request #19419 from abpframework/salihozkara/docsImprovements

Docs Module Improvements and Add Single Project Mode
pull/20186/head
Gizem Mutu Kurt 2 years ago
committed by GitHub
parent
commit
b09fd500f3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      modules/docs/app/VoloDocs.Web/Pages/Index.cshtml.cs
  2. 11
      modules/docs/app/VoloDocs.Web/VoloDocsWebModule.cs
  3. 98
      modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs
  4. 12
      modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/NavigationNode.cs
  5. 7
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/IDocumentRepository.cs
  6. 12
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs
  7. 10
      modules/docs/src/Volo.Docs.EntityFrameworkCore/Volo/Docs/Documents/EFCoreDocumentRepository.cs
  8. 10
      modules/docs/src/Volo.Docs.MongoDB/Volo/Docs/Documents/MongoDocumentRepository.cs
  9. 21
      modules/docs/src/Volo.Docs.Web/Areas/Documents/TagHelpers/TreeTagHelper.cs
  10. 15
      modules/docs/src/Volo.Docs.Web/DocsUiOptions.cs
  11. 29
      modules/docs/src/Volo.Docs.Web/DocsWebModule.cs
  12. 2
      modules/docs/src/Volo.Docs.Web/HtmlConverting/IDocumentToHtmlConverter.cs
  13. 128
      modules/docs/src/Volo.Docs.Web/Markdown/MarkdownDocumentToHtmlConverter.cs
  14. 7
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Index.cshtml.cs
  15. 12
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml
  16. 242
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs
  17. 9
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Search.cshtml
  18. 29
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Search.cshtml.cs
  19. 31
      modules/docs/src/Volo.Docs.Web/Utils/DefaultDocsLinkGenerator.cs
  20. 6
      modules/docs/src/Volo.Docs.Web/Utils/IDocsLinkGenerator.cs

27
modules/docs/app/VoloDocs.Web/Pages/Index.cshtml.cs

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -26,6 +25,10 @@ namespace VoloDocs.Web.Pages
public virtual async Task<IActionResult> OnGetAsync()
{
if (_urlUiOptions.SingleProjectMode.Enable)
{
return Redirect(GetUrlForProject());
}
var projects = await _projectAppService.GetListAsync();
if (projects.Items.Count == 1)
@ -47,13 +50,23 @@ namespace VoloDocs.Web.Pages
}
//Eg: "/en/abp/latest"
public string GetUrlForProject(ProjectDto project, string language = "en", string version = null)
public string GetUrlForProject(ProjectDto project = null, string language = "en", string version = null)
{
return "." +
_urlUiOptions.RoutePrefix.EnsureStartsWith('/').EnsureEndsWith('/') +
language.EnsureEndsWith('/') +
project.ShortName.EnsureEndsWith('/') +
(version ?? DocsAppConsts.Latest);
var routeValues = new Dictionary<string, object> {
{ nameof(Volo.Docs.Pages.Documents.Project.IndexModel.Version), version ?? DocsAppConsts.Latest }
};
if (!_urlUiOptions.SingleProjectMode.Enable)
{
routeValues.Add(nameof(Volo.Docs.Pages.Documents.Project.IndexModel.ProjectName), project?.ShortName);
}
if(_urlUiOptions.MultiLanguageMode)
{
routeValues.Add(nameof(Volo.Docs.Pages.Documents.Project.IndexModel.LanguageCode), language);
}
return Url.Page("/Documents/Project/Index", routeValues);
}
}
}

11
modules/docs/app/VoloDocs.Web/VoloDocsWebModule.cs

@ -75,10 +75,13 @@ namespace VoloDocs.Web
var hostingEnvironment = context.Services.GetHostingEnvironment();
var configuration = context.Services.GetConfiguration();
Configure<DocsUiOptions>(options =>
{
options.RoutePrefix = null;
});
// Configure<DocsUiOptions>(options =>
// {
// options.RoutePrefix = null;
// options.SingleProjectMode.Enable = true;
// options.SingleProjectMode.ProjectName = "abp";
// options.MultiLanguageMode = false;
// });
Configure<DocsElasticSearchOptions>(options =>
{

98
modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -305,7 +306,7 @@ namespace Volo.Docs.Documents
return normalizedPathStringBuilder.ToString();
}
private string RemoveFileExtensionFromPath(string path, string format)
private static string RemoveFileExtensionFromPath(string path, string format)
{
if (path == null)
{
@ -376,19 +377,29 @@ namespace Volo.Docs.Documents
{
return await GetDocumentAsync(documentName, project, languageCode, version);
}
var document = await _documentRepository.FindAsync(project.Id, documentName, languageCode, version);
var document = await _documentRepository.FindAsync(project.Id, GetPossibleNames(documentName, project.Format), languageCode, version);
if (document == null)
{
return await GetDocumentAsync(documentName, project, languageCode, version);
}
if (document.LastCachedTime + _cacheTimeout < DateTime.Now)
if (document.LastCachedTime + _cacheTimeout >= DateTime.Now)
{
return await GetDocumentAsync(documentName, project, languageCode, version, document);
return CreateDocumentWithDetailsDto(project, document);
}
return CreateDocumentWithDetailsDto(project, document);
try
{
return await GetDocumentAsync(documentName, project, languageCode, version, document);
}
catch
{
Logger.LogWarning(
"Could not retrieve the document ({documentName}, {languageCode}, {version}) from the source. Using the cached version.",
documentName, languageCode, version);
return CreateDocumentWithDetailsDto(project, document);
}
}
protected virtual DocumentWithDetailsDto CreateDocumentWithDetailsDto(Project project, Document document)
@ -406,10 +417,8 @@ namespace Volo.Docs.Documents
string languageCode, string version, Document oldDocument = null)
{
Logger.LogInformation($"Not found in the cache. Requesting {documentName} from the source...");
var source = _documentStoreFactory.Create(project.DocumentStoreType);
var sourceDocument = await source.GetDocumentAsync(project, documentName, languageCode, version,
oldDocument?.LastSignificantUpdateTime);
var sourceDocument = await GetSourceDocument(project, documentName, languageCode, version, oldDocument);
await _documentRepository.DeleteAsync(project.Id, sourceDocument.Name, sourceDocument.LanguageCode, sourceDocument.Version, autoSave: true);
await _documentRepository.InsertAsync(sourceDocument, true);
@ -434,6 +443,75 @@ namespace Volo.Docs.Documents
return CreateDocumentWithDetailsDto(project, sourceDocument);
}
private static List<string> GetPossibleNames(string originalDocumentName, string format)
{
var extension = Path.GetExtension(originalDocumentName);
if (extension != null && !extension.Equals("." + format, StringComparison.OrdinalIgnoreCase))
{
return new List<string> {originalDocumentName};
}
var lowerCaseIndex = "index." + format;
var titleCaseIndex = "Index." + format;
var indexLength = lowerCaseIndex.Length;
var possibleNames = new List<string> {originalDocumentName};
if (originalDocumentName.EndsWith("/" + lowerCaseIndex, StringComparison.OrdinalIgnoreCase) || originalDocumentName.Equals(lowerCaseIndex, StringComparison.OrdinalIgnoreCase))
{
var indexPart = originalDocumentName.Right(indexLength);
var documentNameWithoutIndex = originalDocumentName.Left(originalDocumentName.Length - lowerCaseIndex.Length);
if(indexPart != lowerCaseIndex)
{
possibleNames.Add(documentNameWithoutIndex + lowerCaseIndex);
}
if(indexPart != titleCaseIndex)
{
possibleNames.Add(documentNameWithoutIndex + titleCaseIndex);
}
}
else
{
var documentNameWithoutExtension = RemoveFileExtensionFromPath(originalDocumentName, format).EnsureEndsWith('/');
possibleNames.Add(documentNameWithoutExtension + lowerCaseIndex);
possibleNames.Add(documentNameWithoutExtension + titleCaseIndex);
}
return possibleNames;
}
private async Task<Document> GetSourceDocument(Project project, string documentName,
string languageCode, string version, Document oldDocument)
{
var source = _documentStoreFactory.Create(project.DocumentStoreType);
Document sourceDocument = null;
Exception firstException = null;
foreach (var name in GetPossibleNames(documentName, project.Format))
{
try
{
sourceDocument = await source.GetDocumentAsync(project, name, languageCode, version,
oldDocument?.LastSignificantUpdateTime);
break;
}
catch (Exception ex)
{
firstException ??= ex;
}
}
if(sourceDocument == null)
{
throw firstException!;
}
return sourceDocument;
}
private TimeSpan GetCacheTimeout()
{
var value = _configuration["Volo.Docs:DocumentCacheTimeoutInterval"];

12
modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/NavigationNode.cs

@ -35,7 +35,17 @@ namespace Volo.Docs.Documents
return false;
}
if (string.Equals(documentName, Path, StringComparison.OrdinalIgnoreCase))
var path = Path ?? string.Empty;
var pathHasExtension = System.IO.Path.HasExtension(path);
if (!pathHasExtension)
{
var extension = System.IO.Path.GetExtension(documentName);
path = path.EnsureEndsWith('/') + "index" + extension;
}
if (string.Equals(documentName, path, StringComparison.OrdinalIgnoreCase))
{
return true;
}

7
modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/IDocumentRepository.cs

@ -26,6 +26,13 @@ namespace Volo.Docs.Documents
string version,
bool includeDetails = true,
CancellationToken cancellationToken = default);
Task<Document> FindAsync(Guid projectId,
List<string> possibleNames,
string languageCode,
string version,
bool includeDetails = true,
CancellationToken cancellationToken = default);
Task DeleteAsync(Guid projectId,
string name,

12
modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs

@ -410,18 +410,6 @@ namespace Volo.Docs.GitHub.Documents
return await _githubRepositoryManager.GetFileRawStringContentAsync(rawDocumentUrl, token, userAgent);
}
catch (HttpRequestException ex)
{
Logger.LogWarning($"Could not retrieve the document ({rawDocumentUrl}) from Github. Trying to get it from database...");
var document = await _documentRepository.FindAsync(project.Id, documentName, languageCode, version);
if (document == null)
{
throw new DocumentNotFoundException(rawDocumentUrl);
}
return document.Content;
}
catch (Exception ex)
{
//TODO: Only handle when document is really not available

10
modules/docs/src/Volo.Docs.EntityFrameworkCore/Volo/Docs/Documents/EFCoreDocumentRepository.cs

@ -182,6 +182,16 @@ namespace Volo.Docs.Documents
GetCancellationToken(cancellationToken));
}
public async Task<Document> FindAsync(Guid projectId, List<string> possibleNames, string languageCode, string version,
bool includeDetails = true, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync()).IncludeDetails(includeDetails)
.FirstOrDefaultAsync(x =>
x.ProjectId == projectId && possibleNames.Contains(x.Name) &&
x.LanguageCode == languageCode && x.Version == version,
GetCancellationToken(cancellationToken));
}
public virtual async Task DeleteAsync(Guid projectId, string name, string languageCode, string version, bool autoSave = false, CancellationToken cancellationToken = default)
{
await DeleteAsync(x =>

10
modules/docs/src/Volo.Docs.MongoDB/Volo/Docs/Documents/MongoDocumentRepository.cs

@ -94,6 +94,16 @@ namespace Volo.Docs.Documents
x.LanguageCode == languageCode &&
x.Version == version, GetCancellationToken(cancellationToken));
}
public virtual async Task<Document> FindAsync(Guid projectId, List<string> possibleNames, string languageCode, string version,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await (await GetMongoQueryableAsync(cancellationToken)).FirstOrDefaultAsync(x => x.ProjectId == projectId &&
possibleNames.Contains(x.Name) &&
x.LanguageCode == languageCode &&
x.Version == version, GetCancellationToken(cancellationToken));
}
public virtual async Task DeleteAsync(Guid projectId, string name, string languageCode, string version, bool autoSave = false, CancellationToken cancellationToken = default)
{

21
modules/docs/src/Volo.Docs.Web/Areas/Documents/TagHelpers/TreeTagHelper.cs

@ -18,6 +18,8 @@ namespace Volo.Docs.Areas.Documents.TagHelpers
private readonly DocsUiOptions _uiOptions;
private readonly IStringLocalizer<DocsResource> _localizer;
private readonly IDocsLinkGenerator _docsLinkGenerator;
private const string LiItemTemplateWithLink = @"<li class='{0}'><span class='plus-icon'><i class='fa fa-{1}'></i></span>{2}{3}</li>";
@ -45,10 +47,11 @@ namespace Volo.Docs.Areas.Documents.TagHelpers
[HtmlAttributeName("language")]
public string LanguageCode { get; set; }
public TreeTagHelper(IOptions<DocsUiOptions> urlOptions, IStringLocalizer<DocsResource> localizer)
public TreeTagHelper(IOptions<DocsUiOptions> urlOptions, IStringLocalizer<DocsResource> localizer, IDocsLinkGenerator docsLinkGenerator)
{
_localizer = localizer;
_uiOptions = urlOptions.Value;
_docsLinkGenerator = docsLinkGenerator;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
@ -161,11 +164,6 @@ namespace Volo.Docs.Areas.Documents.TagHelpers
private string NormalizePath(string path)
{
if (UrlHelper.IsExternalLink(path))
{
return path;
}
var pathWithoutFileExtension = RemoveFileExtensionFromPath(path);
if (string.IsNullOrWhiteSpace(path))
@ -173,10 +171,13 @@ namespace Volo.Docs.Areas.Documents.TagHelpers
return "javascript:;";
}
var prefix = _uiOptions.RoutePrefix;
var sb = new StringBuilder();
return sb.Append(prefix).Append(LanguageCode).Append("/").Append(ProjectName).Append("/").Append(Version).Append("/").Append(pathWithoutFileExtension).ToString();
if (!UrlHelper.IsExternalLink(path))
{
path = _docsLinkGenerator.GenerateLink(ProjectName, LanguageCode, Version, pathWithoutFileExtension);
}
return path;
}
private string RemoveFileExtensionFromPath(string path)

15
modules/docs/src/Volo.Docs.Web/DocsUiOptions.cs

@ -32,6 +32,10 @@ namespace Volo.Docs
/// Default value: True;
/// </summary>
public bool SectionRendering = true;
public bool MultiLanguageMode { get; set; } = true;
public SingleProjectModeOptions SingleProjectMode { get; } = new ();
private string GetFormattedRoutePrefix()
{
@ -43,4 +47,15 @@ namespace Volo.Docs
return _routePrefix.EnsureEndsWith('/').EnsureStartsWith('/');
}
}
public class SingleProjectModeOptions
{
/// <summary>
/// Determines whether to enable single project mode by removing the project name from the routing.
/// When enabled, only a single project is allowed within the module.
/// </summary>
public bool Enable { get; set; }
public string ProjectName { get; set; }
}
}

29
modules/docs/src/Volo.Docs.Web/DocsWebModule.cs

@ -60,11 +60,32 @@ namespace Volo.Docs
.Value.Value;
var routePrefix = docsOptions.RoutePrefix;
var languageCode = docsOptions.MultiLanguageMode ? "{languageCode}/" : string.Empty;
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + "{projectName}");
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + "{languageCode}/{projectName}");
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + "{languageCode}/{projectName}/{version}/{*documentName}");
options.Conventions.AddPageRoute("/Documents/Search", routePrefix + "search/{languageCode}/{projectName}/{version}");
if (docsOptions.SingleProjectMode.Enable)
{
if (routePrefix != "/")
{
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix);
}
if(routePrefix + languageCode != "/")
{
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + languageCode);
}
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + languageCode + "{version}/{*documentName}");
options.Conventions.AddPageRoute("/Documents/Search", routePrefix + "search/" + languageCode + "{version}");
}
else
{
if(routePrefix + languageCode != "/")
{
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + "{projectName}");
}
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + languageCode + "{projectName}");
options.Conventions.AddPageRoute("/Documents/Project/Index", routePrefix + languageCode + "{projectName}/{version}/{*documentName}");
options.Conventions.AddPageRoute("/Documents/Search", routePrefix + "search/" + languageCode + "{projectName}/{version}");
}
});
context.Services.AddAutoMapperObjectMapper<DocsWebModule>();

2
modules/docs/src/Volo.Docs.Web/HtmlConverting/IDocumentToHtmlConverter.cs

@ -5,6 +5,6 @@ namespace Volo.Docs.HtmlConverting
{
public interface IDocumentToHtmlConverter
{
string Convert(ProjectDto project, DocumentWithDetailsDto document, string version, string languageCode);
string Convert(ProjectDto project, DocumentWithDetailsDto document, string version, string languageCode, string projectShortName = null);
}
}

128
modules/docs/src/Volo.Docs.Web/Markdown/MarkdownDocumentToHtmlConverter.cs

@ -1,10 +1,14 @@
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Docs.Documents;
using Volo.Docs.HtmlConverting;
using Volo.Docs.Pages.Documents.Project;
using Volo.Docs.Projects;
using Volo.Docs.Utils;
@ -16,20 +20,22 @@ namespace Volo.Docs.Markdown
private readonly IMarkdownConverter _markdownConverter;
private readonly DocsUiOptions _uiOptions;
private readonly IDocsLinkGenerator _docsLinkGenerator;
public MarkdownDocumentToHtmlConverter(IMarkdownConverter markdownConverter,
IOptions<DocsUiOptions> urlOptions)
IOptions<DocsUiOptions> urlOptions, IDocsLinkGenerator docsLinkGenerator)
{
_markdownConverter = markdownConverter;
_docsLinkGenerator = docsLinkGenerator;
_uiOptions = urlOptions.Value;
}
private const string MdLinkFormat = "[{0}]({1}{2}/{3}/{4}{5}/{6})";
private const string MdLinkFormat = "[{0}]({1})";
private const string MarkdownLinkRegExp = @"\[(.*?)\]\(((.*?)(\?(.*?))*?)\)";
private const string AnchorLinkRegExp = @"<a[^>]+href=\""(.*?)\""[^>]*>(.*)?</a>";
public virtual string Convert(ProjectDto project, DocumentWithDetailsDto document, string version,
string languageCode)
string languageCode, string projectShortName = null)
{
if (document.Content.IsNullOrEmpty())
{
@ -38,10 +44,10 @@ namespace Volo.Docs.Markdown
var content = NormalizeLinks(
document.Content,
project.ShortName,
_uiOptions.SingleProjectMode.Enable ? projectShortName : projectShortName ?? project.ShortName,
version,
document.LocalDirectory,
languageCode
!_uiOptions.MultiLanguageMode ? languageCode : languageCode ?? document.LanguageCode
);
var html = _markdownConverter.ConvertToHtml(content);
@ -71,91 +77,61 @@ namespace Volo.Docs.Markdown
string documentLocalDirectory,
string languageCode)
{
return Regex.Replace(content, MarkdownLinkRegExp, delegate (Match match)
{
var link = match.Groups[3].Value;
var hashPart = "";
if (link.Contains("#"))
{
var linkSplitted = link.Split("#");
link = linkSplitted[0];
hashPart = linkSplitted[1];
}
if (UrlHelper.IsExternalLink(link))
{
return match.Value;
}
if (!link.EndsWith(".md"))
{
return match.Value;
}
var displayText = match.Groups[1].Value;
var documentName = RemoveFileExtension(link);
var hasUrlParameter = match.Groups.Count > 3 && !match.Groups[4].Value.IsNullOrEmpty();
if (hasUrlParameter)
{
documentName += match.Groups[4].Value;
}
var documentLocalDirectoryNormalized = documentLocalDirectory.TrimStart('/').TrimEnd('/');
if (!string.IsNullOrWhiteSpace(documentLocalDirectoryNormalized))
{
documentLocalDirectoryNormalized = "/" + documentLocalDirectoryNormalized;
}
if (!string.IsNullOrEmpty(hashPart))
{
documentName += $"#{hashPart}";
}
return string.Format(
MdLinkFormat,
displayText,
_uiOptions.RoutePrefix,
languageCode,
projectShortName,
version,
documentLocalDirectoryNormalized,
documentName
);
});
return NormalizeLinksByRegexPattern(MarkdownLinkRegExp, 3, 1, content, projectShortName, version, documentLocalDirectory, languageCode);
}
private string NormalizeAnchorLinks(string projectShortName, string version, string documentLocalDirectory,
string languageCode, string normalized)
{
return Regex.Replace(normalized, AnchorLinkRegExp, delegate (Match match)
return NormalizeLinksByRegexPattern(AnchorLinkRegExp, 1, 2, normalized, projectShortName, version, documentLocalDirectory, languageCode);
}
private string NormalizeLinksByRegexPattern(string regexPattern,
int linkGroupIndex,
int displayTextGroupIndex,
string content,
string projectShortName,
string version,
string documentLocalDirectory,
string languageCode)
{
return Regex.Replace(content, regexPattern, delegate (Match match)
{
var link = match.Groups[1].Value;
if (UrlHelper.IsExternalLink(link))
var link = match.Groups[linkGroupIndex].Value;
var displayText = match.Groups[displayTextGroupIndex].Value;
var hashPart = string.Empty;
var linkPart = link;
if (link.Contains('#'))
{
var linkSplitted = link.Split('#');
linkPart = linkSplitted[0];
hashPart = linkSplitted[1];
}
var documentName = RemoveFileExtension(linkPart);
var isFolder = !linkPart.IsNullOrWhiteSpace() && !Path.HasExtension(linkPart);
var isMdFile = linkPart.EndsWith(".md");
if (UrlHelper.IsExternalLink(link) || !(isMdFile || isFolder))
{
return match.Value;
}
var displayText = match.Groups[2].Value;
var documentName = RemoveFileExtension(link);
var documentLocalDirectoryNormalized = documentLocalDirectory.TrimStart('/').TrimEnd('/');
if (!string.IsNullOrWhiteSpace(documentLocalDirectoryNormalized))
{
documentLocalDirectoryNormalized = "/" + documentLocalDirectoryNormalized;
}
return string.Format(
MdLinkFormat,
displayText,
_uiOptions.RoutePrefix,
languageCode,
projectShortName,
version,
documentLocalDirectoryNormalized,
documentName
);
var hasUrlParameter = match.Groups.Count > 3 && !match.Groups[4].Value.IsNullOrEmpty();
return string.Format(MdLinkFormat, displayText,
_docsLinkGenerator.GenerateLink(projectShortName, languageCode, $"{version}{documentLocalDirectoryNormalized}", documentName)
+ (hasUrlParameter ? match.Groups[4].Value : string.Empty)
+ (hashPart.IsNullOrWhiteSpace() ? string.Empty : "#" + hashPart));
});
}

7
modules/docs/src/Volo.Docs.Web/Pages/Documents/Index.cshtml.cs

@ -27,13 +27,16 @@ namespace Volo.Docs.Pages.Documents
public virtual async Task<IActionResult> OnGetAsync()
{
DocumentsUrlPrefix = _uiOptions.RoutePrefix;
if (_uiOptions.SingleProjectMode.Enable)
{
return RedirectToPage("/Documents/Project/Index");
}
var listResult = await _projectAppService.GetListAsync();
if (listResult.Items.Count == 1)
{
return Redirect(DocumentsUrlPrefix + listResult.Items[0].ShortName);
return RedirectToPage("/Documents/Project/Index", new { shortName = listResult.Items[0].ShortName });
}
Projects = listResult.Items;

12
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml

@ -262,7 +262,10 @@
<input class="form-control"
id="fullsearch"
type="search"
data-fullsearch-url="/search/@Model.LanguageCode/@Model.ProjectName/@Model.Version/"
data-fullsearch-url="@Url.Page("/Documents/Search", new Dictionary<string, object> {
{ nameof(Model.LanguageCode), Model.LanguageCode },
{ nameof(Model.Version), Model.Version },
{ nameof(Model.ProjectName), Model.ProjectName }})"
placeholder="@L["FullSearch"].Value"
aria-label="Filter">
</div>
@ -381,7 +384,7 @@
<div data-spy="scroll" data-bs-target="#docs-sticky-index" data-offset="0">
<article class="docs-body">
@if (Model.DocumentLanguageCode != Model.LanguageCode)
@if (Model.LanguageCode != null && Model.DocumentLanguageCode != Model.LanguageCode)
{
<abp-alert alert-type="Warning" dismissible="true" class="mb-0">
@L["DocumentNotFoundInSelectedLanguage"]
@ -480,7 +483,7 @@ else
{
model = new ErrorPageModel
{
RedirectUrl = Model.DocumentsUrlPrefix,
RedirectUrl = Url.Page("/Projects/Index"),
ErrorCode = "404",
ErrorMessage = L.GetString("ProjectNotFound")
}
@ -493,8 +496,7 @@ else
{
model = new ErrorPageModel
{
RedirectUrl = Model.DocumentsUrlPrefix + Model.LanguageCode + "/" + Model.ProjectName + "/"
+ (Model.LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Model.Version),
RedirectUrl = Model.BuildDocumentUrl(Model.ProjectName, Model.LanguageCode, Model.LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Model.Version, null),
ErrorCode = "404",
ErrorMessage = L.GetString("DocumentNotFound"),
AutoRedirect = !Model.DocumentName.IsNullOrWhiteSpace()

242
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.Application.Dtos;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.Domain.Entities;
using Volo.Docs.Documents;
@ -20,6 +21,7 @@ using Volo.Docs.Models;
using Volo.Docs.Projects;
using Volo.Docs.GitHub.Documents.Version;
using Volo.Docs.Localization;
using Volo.Docs.Utils;
namespace Volo.Docs.Pages.Documents.Project
{
@ -63,8 +65,6 @@ namespace Volo.Docs.Pages.Documents.Project
public VersionInfoViewModel LatestVersionInfo { get; private set; }
public string DocumentsUrlPrefix { get; set; }
public bool ShowProjectsCombobox { get; set; }
public bool ShowProjectsComboboxLabel { get; set; }
@ -91,6 +91,8 @@ namespace Volo.Docs.Pages.Documents.Project
private readonly IProjectAppService _projectAppService;
private readonly IDocumentSectionRenderer _documentSectionRenderer;
private readonly DocsUiOptions _uiOptions;
protected IDocsLinkGenerator DocsLinkGenerator => LazyServiceProvider.LazyGetRequiredService<IDocsLinkGenerator>();
public IndexModel(
IDocumentAppService documentAppService,
@ -119,44 +121,26 @@ namespace Volo.Docs.Pages.Documents.Project
{
return Redirect(decodedUrl);
}
var documentPath = DocumentName ?? "";
string[] documentNames;
if (displayUrl.EndsWith("/index", StringComparison.OrdinalIgnoreCase))
{
documentPath = documentPath.Substring(0, documentPath.LastIndexOf('/') + 1);
}
documentNames = new[] { DocumentName, documentPath.EnsureEndsWith('/') + "Index", documentPath.EnsureEndsWith('/') + "index" };
foreach (var documentName in documentNames)
{
DocumentName = documentName;
try
{
return await SetPageAsync();
}
catch (DocumentNotFoundException exception)
{
Logger.LogWarning(exception.Message);
}
}
DocumentFound = false;
Response.StatusCode = 404;
return Page();
return await SetPageAsync();
}
private async Task<IActionResult> SetPageAsync()
{
DocumentsUrlPrefix = _uiOptions.RoutePrefix;
ShowProjectsCombobox = _uiOptions.ShowProjectsCombobox;
ShowProjectsCombobox = _uiOptions.ShowProjectsCombobox && !_uiOptions.SingleProjectMode.Enable;
ShowProjectsComboboxLabel = ShowProjectsCombobox && _uiOptions.ShowProjectsComboboxLabel;
FullSearchEnabled = await _documentAppService.FullSearchEnabledAsync();
try
{
await SetProjectAsync();
if (Project == null)
{
ProjectFound = false;
Response.StatusCode = 404;
return Page();
}
}
catch (EntityNotFoundException e)
{
@ -171,25 +155,36 @@ namespace Volo.Docs.Pages.Documents.Project
await SetProjectsAsync();
}
await SetVersionAsync();
if (!await TrySetVersionAsync())
{
return RedirectToDefaultVersion();
}
await SetLanguageList();
if (IsDefaultDocument())
{
return RedirectToDefaultDocument();
}
var usingSingleLanguageMode = !_uiOptions.MultiLanguageMode && LanguageCode.IsNullOrWhiteSpace();
if (!CheckLanguage())
if (!usingSingleLanguageMode && !CheckLanguage())
{
return RedirectToDefaultLanguage();
}
if (IsDocumentCultureDifferentThanCurrent())
if (!usingSingleLanguageMode && IsDocumentCultureDifferentThanCurrent())
{
return ReloadPageWithCulture();
}
await SetDocumentAsync();
if (!await TrySetDocumentAsync())
{
DocumentFound = false;
Response.StatusCode = 404;
return Page();
}
await SetNavigationAsync();
SetLanguageSelectListItems();
@ -217,12 +212,29 @@ namespace Volo.Docs.Pages.Documents.Project
private async Task SetProjectAsync()
{
Project = await _projectAppService.GetAsync(ProjectName);
if (!_uiOptions.SingleProjectMode.Enable)
{
Project = await _projectAppService.GetAsync(ProjectName);
return;
}
var singleProjectName = ProjectName ?? _uiOptions.SingleProjectMode.ProjectName;
if (!singleProjectName.IsNullOrWhiteSpace())
{
Project = await _projectAppService.GetAsync(singleProjectName);
return;
}
var listResult = await _projectAppService.GetListAsync();
if (listResult.Items.Count == 1)
{
Project = listResult.Items[0];
}
}
private async Task SetLanguageList()
{
LanguageConfig = await _projectAppService.GetLanguageListAsync(ProjectName, Version);
LanguageConfig = await _projectAppService.GetLanguageListAsync(Project.ShortName, Version);
SetDefaultLanguageCode();
}
@ -233,61 +245,62 @@ namespace Volo.Docs.Pages.Documents.Project
private bool CheckLanguage()
{
return LanguageConfig.Languages.Any(l => l.Code == LanguageCode);
return LanguageConfig.Languages.Any(l => l.Code.Equals(LanguageCode, StringComparison.OrdinalIgnoreCase));
}
private IActionResult ReloadPageWithCulture()
{
var sb = new StringBuilder();
var returnUrl = sb.Append(DocumentsUrlPrefix).Append(LanguageCode).Append("/").Append(ProjectName)
.Append("/").Append(LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Version).Append("/").Append(DocumentName).ToString();
sb.Clear();
return Redirect(sb.Append("/Abp/Languages/Switch?culture=").Append(LanguageCode).Append("&uiCulture=")
.Append(LanguageCode).Append("&returnUrl=").Append(returnUrl).ToString());
return Redirect(
new StringBuilder()
.Append("/Abp/Languages/Switch?culture=")
.Append(LanguageCode).Append("&uiCulture=")
.Append(LanguageCode).Append("&returnUrl=").Append(DocsLinkGenerator.GenerateLink(ProjectName, LanguageCode,
LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Version, DocumentName)).ToString());
}
public string GetFullUrlOfTheLatestDocument()
{
return Request.Scheme + "://" + Request.Host.Value + Request.PathBase +
DocumentsUrlPrefix + LanguageCode + "/" + ProjectName + "/" +
DocsAppConsts.Latest + "/" + DocumentName;
return Request.Scheme + Uri.SchemeDelimiter + Request.Host.Value + Request.PathBase + DocsLinkGenerator.GenerateLink(ProjectName, LanguageCode, DocsAppConsts.Latest, DocumentName);
}
public string CreateDocumentLink(string documentName)
{
return new StringBuilder()
.Append(DocumentsUrlPrefix)
.Append(LanguageCode)
.Append('/')
.Append(ProjectName)
.Append('/')
.Append(LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Version)
.Append('/')
.Append(documentName)
.ToString();
return DocsLinkGenerator.GenerateLink(ProjectName, LanguageCode, LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Version, documentName);
}
public string BuildDocumentUrl(string projectName, string languageCode, string version, string documentName)
{
return DocsLinkGenerator.GenerateLink(projectName, languageCode, version, documentName);
}
private IActionResult RedirectToDefaultLanguage()
{
return RedirectToPage(new {
projectName = ProjectName,
version = (LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Version),
languageCode = DefaultLanguageCode,
documentName = DocumentName
});
return Redirect(DocsLinkGenerator.GenerateLink(
ProjectName,
DefaultLanguageCode,
LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Version,
DocumentName
));
}
private IActionResult RedirectToDefaultVersion()
{
return Redirect(DocsLinkGenerator.GenerateLink(
ProjectName,
LanguageCode,
DocsAppConsts.Latest,
DocumentName
));
}
private IActionResult RedirectToDefaultDocument()
{
return RedirectToPage(new {
projectName = ProjectName,
version = (LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Version),
documentName = "",
languageCode = DefaultLanguageCode
});
return Redirect(DocsLinkGenerator.GenerateLink (
ProjectName,
DefaultLanguageCode,
LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Version,
""
));
}
private async Task SetProjectsAsync()
@ -308,18 +321,10 @@ namespace Volo.Docs.Pages.Documents.Project
{
return null;
}
return new StringBuilder()
.Append(DocumentsUrlPrefix)
.Append(LanguageCode)
.Append('/')
.Append(project.ShortName)
.Append('/')
.Append(DocsAppConsts.Latest)
.ToString();
return DocsLinkGenerator.GenerateLink(project.ShortName, LanguageCode, Version, null);
}
private async Task SetVersionAsync()
private async Task<bool> TrySetVersionAsync()
{
//TODO: Needs refactoring
if (string.IsNullOrWhiteSpace(Version))
@ -356,8 +361,7 @@ namespace Volo.Docs.Pages.Documents.Project
}
else
{
LatestVersionInfo.IsSelected = true;
Version = LatestVersionInfo.Version;
return false;
}
}
}
@ -382,6 +386,8 @@ namespace Volo.Docs.Pages.Documents.Project
}).ToList();
IsLatestVersion = Version == LatestVersionInfo.Version;
return true;
}
private VersionInfoViewModel GetLatestVersionInfo(List<VersionInfoViewModel> versions)
@ -441,7 +447,7 @@ namespace Volo.Docs.Pages.Documents.Project
new GetNavigationDocumentInput
{
ProjectId = Project.Id,
LanguageCode = LanguageCode,
LanguageCode = DocumentLanguageCode,
Version = Version
}
);
@ -458,16 +464,8 @@ namespace Volo.Docs.Pages.Documents.Project
{
version = DocsAppConsts.Latest;
}
var linkStringBuilder = new StringBuilder();
linkStringBuilder.Append(DocumentsUrlPrefix).Append(LanguageCode).Append("/").Append(ProjectName).Append("/").Append(version);
if (documentName != null)
{
linkStringBuilder.Append("/").Append(DocumentName);
}
return linkStringBuilder.ToString();
return DocsLinkGenerator.GenerateLink(ProjectName, LanguageCode, version, documentName);
}
public string GetSpecificVersionOrLatest()
@ -482,50 +480,47 @@ namespace Volo.Docs.Pages.Documents.Project
RemoveVersionPrefix(Document.Version);
}
private async Task SetDocumentAsync()
private async Task<bool> TrySetDocumentAsync()
{
var sb = new StringBuilder();
DocumentNameWithExtension = sb.Append(DocumentName).Append(".").Append(Project.Format).ToString();
try
{
Document = await GetSpecificDocumentOrDefaultAsync(LanguageCode);
DocumentLanguageCode = LanguageCode;
}
catch (DocumentNotFoundException)
var languages = new[] { LanguageCode, DefaultLanguageCode }.Where(x => !x.IsNullOrWhiteSpace()).Distinct().ToArray();
DocumentNameWithExtension = new StringBuilder().Append(DocumentName).Append('.').Append(Project.Format).ToString();
foreach (var language in languages)
{
if (LanguageCode != DefaultLanguageCode)
try
{
Document = await GetSpecificDocumentOrDefaultAsync(DefaultLanguageCode);
DocumentLanguageCode = DefaultLanguageCode;
Document = await GetSpecificDocumentOrDefaultAsync(language);
DocumentLanguageCode = language;
DocumentNameWithExtension = Document.Name;
await ConvertDocumentContentToHtmlAsync();
return true;
}
else
catch (DocumentNotFoundException e)
{
throw;
Logger.LogWarning(e.Message);
}
}
await ConvertDocumentContentToHtmlAsync();
return false;
}
private void SetLanguageSelectListItems()
{
LanguageSelectListItems = new List<SelectListItem>();
var sb = new StringBuilder();
if (!_uiOptions.MultiLanguageMode)
{
return;
}
foreach (var language in LanguageConfig.Languages)
{
LanguageSelectListItems.Add(
new SelectListItem(
language.DisplayName,
sb.Append(DocumentsUrlPrefix).Append(language.Code).Append("/").Append(Project.ShortName).Append("/").Append(LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Version).Append("/").Append(DocumentName).ToString(),
language.Code == LanguageCode
)
);
sb.Clear();
DocsLinkGenerator.GenerateLink(ProjectName, language.Code, LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Version, DocumentName),
language.Code.Equals(LanguageCode, StringComparison.OrdinalIgnoreCase)
)
);
}
}
@ -545,7 +540,7 @@ namespace Volo.Docs.Pages.Documents.Project
}
var converter = _documentToHtmlConverterFactory.Create(Document.Format ?? Project.Format);
var content = converter.Convert(Project, Document, GetSpecificVersionOrLatest(), LanguageCode);
var content = converter.Convert(Project, Document, GetSpecificVersionOrLatest(), LanguageCode, ProjectName);
content = HtmlNormalizer.ReplaceImageSources(
content,
@ -578,14 +573,7 @@ namespace Volo.Docs.Pages.Documents.Project
{
foreach (var parameter in partialTemplates.Parameters)
{
if (!UserPreferences.ContainsKey(parameter.Key))
{
UserPreferences.Add(parameter.Key, parameter.Value);
}
else
{
UserPreferences[parameter.Key] = parameter.Value;
}
UserPreferences[parameter.Key] = parameter.Value;
}
}

9
modules/docs/src/Volo.Docs.Web/Pages/Documents/Search.cshtml

@ -29,7 +29,7 @@
<div class="container mb-5">
<form method="get" action="/search/@Model.LanguageCode/@Model.ProjectName/@Model.Version/" class="mt-4">
<form method="get" action="@Url.Page("/Documents/Search", new Dictionary<string, object> { { nameof(Model.LanguageCode), Model.LanguageCode }, { nameof(Model.Version), Model.Version }, { nameof(Model.ProjectName), Model.ProjectName } })" class="mt-4">
<h4 class="text-center my-3">@L["FullSearch"]</h4>
<div class="card rounded">
<div class="p-3 bg-white">
@ -68,7 +68,12 @@
}
}
<h5 class="mb-3">
<a href="/@Model.LanguageCode/@Model.ProjectName/@Model.Version/@RemoveFileExtensionFromPath(docs.Name)">
<a href="@Url.Page("/Documents/Project/Index", new Dictionary<string, object> {
{ nameof(Volo.Docs.Pages.Documents.Project.IndexModel.LanguageCode), Model.LanguageCode },
{ nameof(Volo.Docs.Pages.Documents.Project.IndexModel.Version), Model.Version },
{ nameof(Volo.Docs.Pages.Documents.Project.IndexModel.ProjectName), Model.ProjectName },
{ nameof(Volo.Docs.Pages.Documents.Project.IndexModel.DocumentName), RemoveFileExtensionFromPath(docs.Name) }
})">
@RemoveFileExtensionFromPath(docs.Name)
</a>
</h5>

29
modules/docs/src/Volo.Docs.Web/Pages/Documents/Search.cshtml.cs

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination;
using Volo.Docs.Documents;
using Volo.Docs.GitHub.Documents.Version;
@ -35,14 +36,16 @@ namespace Volo.Docs.Pages.Documents
private readonly IProjectAppService _projectAppService;
private readonly IDocumentAppService _documentAppService;
private readonly HtmlEncoder _encoder;
private readonly DocsUiOptions _uiOptions;
public SearchModel(IProjectAppService projectAppService,
IDocumentAppService documentAppService,
HtmlEncoder encoder)
HtmlEncoder encoder, IOptions<DocsUiOptions> uiOptions)
{
_projectAppService = projectAppService;
_documentAppService = documentAppService;
_encoder = encoder;
_uiOptions = uiOptions.Value;
}
public List<DocumentSearchOutput> SearchOutputs { get; set; } = new List<DocumentSearchOutput>();
@ -61,7 +64,7 @@ namespace Volo.Docs.Pages.Documents
KeyWord = keyword;
Project = await _projectAppService.GetAsync(ProjectName);
await SetProjectAsync();
var output = await _projectAppService.GetVersionsAsync(Project.ShortName);
@ -117,5 +120,27 @@ namespace Volo.Docs.Pages.Documents
return Page();
}
private async Task SetProjectAsync()
{
if (!_uiOptions.SingleProjectMode.Enable)
{
Project = await _projectAppService.GetAsync(ProjectName);
return;
}
var singleProjectName = ProjectName ?? _uiOptions.SingleProjectMode.ProjectName;
if (!singleProjectName.IsNullOrWhiteSpace())
{
Project = await _projectAppService.GetAsync(singleProjectName);
return;
}
var listResult = await _projectAppService.GetListAsync();
if (listResult.Items.Count == 1)
{
Project = listResult.Items[0];
}
}
}
}

31
modules/docs/src/Volo.Docs.Web/Utils/DefaultDocsLinkGenerator.cs

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Net;
using Microsoft.AspNetCore.Routing;
using Volo.Abp.DependencyInjection;
using Volo.Docs.Pages.Documents.Project;
namespace Volo.Docs.Utils;
public class DefaultDocsLinkGenerator : IDocsLinkGenerator, ITransientDependency
{
protected LinkGenerator LinkGenerator { get; }
public DefaultDocsLinkGenerator(LinkGenerator linkGenerator)
{
LinkGenerator = linkGenerator;
}
public string GenerateLink(string projectName, string languageCode, string version, string documentName)
{
var routeValues = new Dictionary<string, object> {
{ nameof(IndexModel.LanguageCode), languageCode },
{ nameof(IndexModel.Version), version },
{ nameof(IndexModel.DocumentName), documentName },
{ nameof(IndexModel.ProjectName), projectName }
};
var encodedUrl = LinkGenerator.GetPathByPage("/Documents/Project/Index", values: routeValues);
return encodedUrl?.Replace("%2F", "/"); //Document name can contain path separator(/), so we need to decode it.
}
}

6
modules/docs/src/Volo.Docs.Web/Utils/IDocsLinkGenerator.cs

@ -0,0 +1,6 @@
namespace Volo.Docs.Utils;
public interface IDocsLinkGenerator
{
string GenerateLink(string projectName, string languageCode, string version, string documentName);
}
Loading…
Cancel
Save