Skip to content

Commit

Permalink
Merge branch 'main' into feat/search-improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
yufeih authored Feb 20, 2025
2 parents 3e393a4 + b0f5472 commit 047f5c5
Show file tree
Hide file tree
Showing 107 changed files with 8,857 additions and 2,936 deletions.
11 changes: 11 additions & 0 deletions .github/codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
coverage:
status:
project:
default:
informational: true
patch:
default:
informational: true
comment: false
github_checks:
annotations: false
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ jobs:
- uses: codecov/codecov-action@v5
if: matrix.os == 'ubuntu-latest'
with:
fail_ci_if_error: true
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}

- run: echo "DOTNET_DbgEnableMiniDump=1" >> $GITHUB_ENV
if: matrix.os == 'ubuntu-latest'
Expand Down
28 changes: 17 additions & 11 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
<PackageVersion Include="ICSharpCode.Decompiler" Version="8.2.0.7535" />
<PackageVersion Include="Jint" Version="4.1.0" />
<PackageVersion Include="JsonSchema.Net" Version="7.2.3" />
<PackageVersion Include="Markdig" Version="0.38.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.49.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.72" />
<PackageVersion Include="ICSharpCode.Decompiler" Version="9.0.0.7889" />
<PackageVersion Include="Jint" Version="4.2.0" />
<PackageVersion Include="JsonSchema.Net" Version="7.3.3" />
<PackageVersion Include="Markdig" Version="0.40.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.50.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="OneOf" Version="3.0.271" />
<PackageVersion Include="OneOf.SourceGenerator" Version="3.0.271" />
Expand All @@ -18,11 +18,17 @@
<PackageVersion Include="Spectre.Console" Version="0.49.1" />
<PackageVersion Include="Spectre.Console.Cli" Version="0.49.1" />
<PackageVersion Include="Stubble.Core" Version="1.10.8" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
<PackageVersion Include="System.Composition" Version="9.0.0" />
<PackageVersion Include="System.Formats.Asn1" Version="9.0.0" />
<PackageVersion Include="System.Text.Json" Version="9.0.0" />
<PackageVersion Include="YamlDotNet" Version="15.3.0" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.2" />
<PackageVersion Include="System.Composition" Version="9.0.2" />
<PackageVersion Include="System.Formats.Asn1" Version="9.0.2" />
<PackageVersion Include="System.Text.Json" Version="9.0.2" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>

<!-- .slnx solution format is supported Microsoft.Build 17.13.9 or later. -->
<ItemGroup>
<PackageVersion Include="Microsoft.Build" Version="[17.11.4]" Condition="'$(TargetFramework)' == 'net8.0'"/>
<PackageVersion Include="Microsoft.Build" Version="17.13.9" Condition="'$(TargetFramework)' != 'net8.0'"/>
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ If you want to use prerelease version, you can install package with following st
2. Login to GitHub with additional scope request

```pwsh
gh auth login --scopes "read:packages" --host github.com
gh auth login --scopes "read:packages" --hostname github.com
```
3. Follow the instructions and complete the login steps.
Expand Down
39 changes: 38 additions & 1 deletion samples/seed/dotnet/project/Project/Inheritdoc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,41 @@ public class Class2 : Class1<bool>
public override bool TestMethod1(bool parm1, int parm2) => false;
}
}
}

// Issue #9736 #9495 #9754
public class Issue9736
{
public interface IJsonApiOptions
{
/// <summary>
/// Whether to use relative links for all resources. <c>false</c> by default.
/// </summary>
/// <example>
/// <code><![CDATA[
/// options.UseRelativeLinks = true;
/// ]]></code>
/// <code><![CDATA[
/// {
/// "type": "articles",
/// "id": "4309",
/// "relationships": {
/// "author": {
/// "links": {
/// "self": "/api/shopping/articles/4309/relationships/author",
/// "related": "/api/shopping/articles/4309/author"
/// }
/// }
/// }
/// }
/// ]]></code>
/// </example>
bool UseRelativeLinks { get; }
}

public sealed class JsonApiOptions : IJsonApiOptions
{
/// <inheritdoc />
public bool UseRelativeLinks { get; set; }
}
}
}
59 changes: 46 additions & 13 deletions src/Docfx.App/PdfBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ class Outline
public string? pdfFooterTemplate { get; init; }
}

public static Task Run(BuildJsonConfig config, string configDirectory, string? outputDirectory = null)
public static Task Run(BuildJsonConfig config, string configDirectory, string? outputDirectory = null, CancellationToken cancellationToken = default)
{
var outputFolder = Path.GetFullPath(Path.Combine(
string.IsNullOrEmpty(outputDirectory) ? Path.Combine(configDirectory, config.Output ?? "") : outputDirectory,
config.Dest ?? ""));

Logger.LogInfo($"Searching for manifest in {outputFolder}");
return CreatePdf(outputFolder);
return CreatePdf(outputFolder, cancellationToken);
}

public static async Task CreatePdf(string outputFolder)
public static async Task CreatePdf(string outputFolder, CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var pdfTocs = GetPdfTocs().ToDictionary(p => p.url, p => p.toc);
Expand All @@ -82,7 +82,7 @@ public static async Task CreatePdf(string outputFolder)
using var app = builder.Build();
app.UseServe(outputFolder);
app.MapGet("/_pdftoc/{*url}", TocPage);
await app.StartAsync();
await app.StartAsync(cancellationToken);

baseUrl = new Uri(app.Urls.First());

Expand All @@ -100,25 +100,51 @@ public static async Task CreatePdf(string outputFolder)
var headerFooterTemplateCache = new ConcurrentDictionary<string, string>();
var headerFooterPageCache = new ConcurrentDictionary<(string, string), Task<byte[]>>();

await AnsiConsole.Progress().StartAsync(async progress =>
var pdfBuildTask = AnsiConsole.Progress().StartAsync(async progress =>
{
await Parallel.ForEachAsync(pdfTocs, async (item, _) =>
await Parallel.ForEachAsync(pdfTocs, new ParallelOptions { CancellationToken = cancellationToken }, async (item, _) =>
{
var (url, toc) = item;
var outputName = Path.Combine(Path.GetDirectoryName(url) ?? "", toc.pdfFileName ?? Path.ChangeExtension(Path.GetFileName(url), ".pdf"));
var task = progress.AddTask(outputName);
var outputPath = Path.Combine(outputFolder, outputName);
var pdfOutputPath = Path.Combine(outputFolder, outputName);

await CreatePdf(
PrintPdf, PrintHeaderFooter, task, new(baseUrl, url), toc, outputFolder, outputPath,
pageNumbers => pdfPageNumbers[url] = pageNumbers);
PrintPdf, PrintHeaderFooter, task, new(baseUrl, url), toc, outputFolder, pdfOutputPath,
pageNumbers => pdfPageNumbers[url] = pageNumbers,
cancellationToken);

task.Value = task.MaxValue;
task.StopTask();
});
});

try
{
await pdfBuildTask.WaitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
if (!pdfBuildTask.IsCompleted)
{
// If pdf generation task is not completed.
// Manually close playwright context/browser to immediately shutdown remaining tasks.
await context.CloseAsync();
await browser.CloseAsync();
try
{
await pdfBuildTask; // Wait AnsiConsole.Progress operation completed to output logs.
}
catch
{
Logger.LogError($"PDF file generation is canceled by user interaction.");
return;
}
}
}

Logger.LogVerbose($"PDF done in {stopwatch.Elapsed}");
return;

IEnumerable<(string url, Outline toc)> GetPdfTocs()
{
Expand Down Expand Up @@ -150,7 +176,7 @@ IResult TocPage(string url)

async Task<byte[]?> PrintPdf(Outline outline, Uri url)
{
await pageLimiter.WaitAsync();
await pageLimiter.WaitAsync(cancellationToken);
var page = pagePool.TryTake(out var pooled) ? pooled : await context.NewPageAsync();

try
Expand Down Expand Up @@ -273,7 +299,7 @@ static string ExpandTemplate(string? pdfTemplate, int pageNumber, int totalPages

static async Task CreatePdf(
Func<Outline, Uri, Task<byte[]?>> printPdf, Func<Outline, int, int, Page, Task<byte[]>> printHeaderFooter, ProgressTask task,
Uri outlineUrl, Outline outline, string outputFolder, string outputPath, Action<Dictionary<Outline, int>> updatePageNumbers)
Uri outlineUrl, Outline outline, string outputFolder, string pdfOutputPath, Action<Dictionary<Outline, int>> updatePageNumbers, CancellationToken cancellationToken)
{
var pages = GetPages(outline).ToArray();
if (pages.Length == 0)
Expand All @@ -284,7 +310,7 @@ static async Task CreatePdf(
// Make progress at 99% before merge PDF
task.MaxValue = pages.Length + (pages.Length / 99.0);

await Parallel.ForEachAsync(pages, async (item, _) =>
await Parallel.ForEachAsync(pages, new ParallelOptions { CancellationToken = cancellationToken }, async (item, _) =>
{
var (url, node) = item;
if (await printPdf(outline, url) is { } bytes)
Expand All @@ -302,6 +328,8 @@ await Parallel.ForEachAsync(pages, async (item, _) =>

foreach (var (url, node) in pages)
{
cancellationToken.ThrowIfCancellationRequested();

if (!pageBytes.TryGetValue(node, out var bytes))
continue;

Expand All @@ -324,13 +352,14 @@ await Parallel.ForEachAsync(pages, async (item, _) =>

var producer = $"docfx ({typeof(PdfBuilder).Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version})";

using var output = File.Create(outputPath);
using var output = File.Create(pdfOutputPath);
using var builder = new PdfDocumentBuilder(output);

builder.DocumentInformation = new() { Producer = producer };
builder.Bookmarks = CreateBookmarks(outline.items);

await MergePdf();
return;

IEnumerable<(Uri url, Outline node)> GetPages(Outline outline)
{
Expand Down Expand Up @@ -368,6 +397,8 @@ async Task MergePdf()

foreach (var (url, node) in pages)
{
cancellationToken.ThrowIfCancellationRequested();

if (!pageBytes.TryGetValue(node, out var bytes))
continue;

Expand All @@ -387,6 +418,8 @@ async Task MergePdf()
using var document = PdfDocument.Open(bytes);
for (var i = 1; i <= document.NumberOfPages; i++)
{
cancellationToken.ThrowIfCancellationRequested();

pageNumber++;

var pageBuilder = builder.AddPage(document, i, x => CopyLink(node, x));
Expand Down
42 changes: 42 additions & 0 deletions src/Docfx.App/RunServe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Docfx.Common;
using Docfx.Plugins;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

#nullable enable

namespace Docfx;

/// <summary>
Expand Down Expand Up @@ -44,6 +48,7 @@ public static void Exec(string folder, string host, int? port, bool openBrowser,
Console.WriteLine($"Serving \"{folder}\" on {url}");
Console.WriteLine("Press Ctrl+C to shut down");
using var app = builder.Build();
app.UseExtensionlessHtmlUrl();
app.UseServe(folder);

if (openBrowser || !string.IsNullOrEmpty(openFile))
Expand Down Expand Up @@ -161,4 +166,41 @@ private static void LaunchBrowser(string url)
Logger.LogError($"Could not launch the browser process. with error - {ex.Message}");
}
}

/// <summary>
/// Enable HTML content access with extensionless URL.
/// This extension method must be called before `UseFileServer` or `UseStaticFiles`.
/// </summary>
private static IApplicationBuilder UseExtensionlessHtmlUrl(this WebApplication app)
{
// Configure middleware that rewrite extensionless url to physical HTML file path.
return app.Use(async (context, next) =>
{
if (IsGetOrHeadMethod(context.Request.Method)
&& TryResolveHtmlFilePath(context.Request.Path, out var htmlFilePath))
{
context.Request.Path = htmlFilePath;
}

await next();
});

static bool IsGetOrHeadMethod(string method) => HttpMethods.IsGet(method) || HttpMethods.IsHead(method);

// Try to resolve HTML file path.
bool TryResolveHtmlFilePath(PathString pathString, [NotNullWhen(true)] out string? htmlPath)
{
var path = pathString.Value;
if (!string.IsNullOrEmpty(path) && !Path.HasExtension(path) && !path.EndsWith('/'))
{
htmlPath = $"{path}.html";
var fileInfo = app.Environment.WebRootFileProvider.GetFileInfo(htmlPath);
if (fileInfo != null)
return true;
}

htmlPath = null;
return false;
}
}
}
7 changes: 5 additions & 2 deletions src/Docfx.Build/HtmlTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@

namespace Docfx.Build;

struct HtmlTemplate
partial struct HtmlTemplate
{
[GeneratedRegex("(\\s+[a-zA-Z0-9_-]+)=([\"']){(\\d)}[\"']")]
private static partial Regex AttributePlaceholderRegex();

private string? _html;

public override string ToString() => _html ?? "";
Expand All @@ -18,7 +21,7 @@ struct HtmlTemplate

public static HtmlTemplate Html(FormattableString template)
{
var format = Regex.Replace(template.Format, "(\\s+[a-zA-Z0-9_-]+)=([\"']){(\\d)}[\"']", RenderAttribute);
var format = AttributePlaceholderRegex().Replace(template.Format, RenderAttribute);
var html = string.Format(format, Array.ConvertAll(template.GetArguments(), Render));
return new() { _html = html };

Expand Down
Loading

0 comments on commit 047f5c5

Please sign in to comment.