Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions samples/seed/articles/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[Markdown](https://daringfireball.net/projects/markdown/) is a lightweight markup language with plain text formatting syntax. Docfx supports [CommonMark](https://commonmark.org/) compliant Markdown parsed through the [Markdig](https://github.com/xoofx/markdig) parsing engine.

Link to [Math Expressions](#math-expressions)

# Block Quotes

> This is a block quote.
Expand Down
39 changes: 29 additions & 10 deletions src/Docfx.App/PdfBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using Docfx.Plugins;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
Expand Down Expand Up @@ -117,6 +118,7 @@ await Parallel.ForEachAsync(pages, async (item, CancellationToken) =>
var task = c.AddTask(item.url.PathAndQuery);
var page = await browser.NewPageAsync();
await page.GotoAsync(item.url.ToString());
await page.AddScriptTagAsync(new() { Content = EnsureHeadingAnchorScript });
var bytes = await page.PdfAsync(new() { Margin = new() { Bottom = margin, Top = margin, Left = margin, Right = margin } });
File.WriteAllBytes(item.path, bytes);
task.Value = task.MaxValue;
Expand All @@ -133,8 +135,9 @@ await Parallel.ForEachAsync(pages, async (item, CancellationToken) =>
var url = new Uri(outlineUrl, outline.href);
if (url.Host == outlineUrl.Host)
{
var id = Convert.ToHexString(SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(url.ToString())));
yield return (Path.Combine(tempDirectory, $"{id}.pdf"), url, outline);
var id = Convert.ToHexString(SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(url.ToString()))).Substring(0, 6).ToLower();
var name = Regex.Replace(url.PathAndQuery, "\\W", "-").Trim('-');
yield return (Path.Combine(tempDirectory, $"{name}-{id}.pdf"), url, outline);
}
}

Expand Down Expand Up @@ -173,19 +176,20 @@ void MergePdf()
// Copy pages
foreach (var (path, url, node) in pages)
{
var basePageNumber = pageNumbers[node] - 1;
using var document = PdfDocument.Open(path);
for (var i = 1; i <= document.NumberOfPages; i++)
builder.AddPage(document, i, CopyLink);
builder.AddPage(document, i, a => CopyLink(a, basePageNumber));
}

builder.Bookmarks = new(CreateBookmarks(outline.items).ToArray());
}

PdfAction CopyLink(PdfAction action)
PdfAction CopyLink(PdfAction action, int basePageNumber)
{
return action switch
{
GoToAction link => new GoToAction(new(link.Destination.PageNumber, link.Destination.Type, link.Destination.Coordinates)),
GoToAction link => new GoToAction(new(basePageNumber + link.Destination.PageNumber, link.Destination.Type, link.Destination.Coordinates)),
UriAction url => HandleUriAction(url),
_ => action,
};
Expand All @@ -200,16 +204,16 @@ PdfAction HandleUriAction(UriAction url)
var name = uri.Fragment.Substring(1);
foreach (var (node, namedDests) in pages)
{
if (namedDests.TryGet(name, out var dest))
if (namedDests.TryGet(name, out var dest) && dest is not null)
{
return new GoToAction(new(1, dest.Type, dest.Coordinates));
return new GoToAction(new(pageNumbers[node] + dest.PageNumber - 1, dest.Type, dest.Coordinates));
}
}

AnsiConsole.MarkupLine($"[yellow]Failed to resolve named dest: {name}[/]");
}

return new GoToAction(new(pageNumbers[pages[0].node], ExplicitDestinationType.FitHorizontally, ExplicitDestinationCoordinates.Empty));
return new GoToAction(new(pageNumbers[pages[0].node], ExplicitDestinationType.XyzCoordinates, ExplicitDestinationCoordinates.Empty));
}
}

Expand All @@ -229,7 +233,7 @@ IEnumerable<BookmarkNode> CreateBookmarks(Outline[]? items, int level = 0)
{
yield return new DocumentBookmarkNode(
item.name, level,
new(nextPageNumber, ExplicitDestinationType.FitHorizontally, ExplicitDestinationCoordinates.Empty),
new(nextPageNumber, ExplicitDestinationType.XyzCoordinates, ExplicitDestinationCoordinates.Empty),
CreateBookmarks(item.items, level + 1).ToArray());
continue;
}
Expand All @@ -246,9 +250,24 @@ IEnumerable<BookmarkNode> CreateBookmarks(Outline[]? items, int level = 0)
nextPageNumber = nextPageNumbers[item];
yield return new DocumentBookmarkNode(
item.name, level,
new(pageNumbers[item], ExplicitDestinationType.FitHorizontally, ExplicitDestinationCoordinates.Empty),
new(pageNumbers[item], ExplicitDestinationType.XyzCoordinates, ExplicitDestinationCoordinates.Empty),
CreateBookmarks(item.items, level + 1).ToArray());
}
}
}

/// <summary>
/// Adds hidden links to headings to ensure Chromium saves heading anchors to named dests
/// for cross page bookmark reference.
/// </summary>
static string EnsureHeadingAnchorScript =>
"""
document.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(h => {
if (h.id) {
const a = document.createElement('a')
a.href = '#' + h.id
document.body.appendChild(a)
}
})
""";
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@
"articles/markdown.html": {
"href": "articles/markdown.html",
"title": "Markdown | docfx seed website",
"keywords": "Markdown Markdown is a lightweight markup language with plain text formatting syntax. Docfx supports CommonMark compliant Markdown parsed through the Markdig parsing engine. Block Quotes This is a block quote. Alerts Note Information the user should notice even if skimming. Tip Optional information to help a user be more successful. Important Essential information required for user success. Caution Negative potential consequences of an action. Warning Dangerous certain consequences of an action. MY TODO This is a TODO. Image Mermaid Diagrams Flowchart flowchart LR A[Hard] -->|Text| B(Round) B --> C{Decision} C -->|One| D[Result 1] C -->|Two| E[Result 2] Code Snippet The example highlights lines 2, line 5 to 7 and lines 9 to the end of the file. using System; using Azure; using Azure.Storage; using Azure.Storage.Blobs; class Program { static void Main(string[] args) { // Define the connection string for the storage account string connectionString = \"DefaultEndpointsProtocol=https;AccountName=<your-account-name>;AccountKey=<your-account-key>;EndpointSuffix=core.windows.net\"; // Create a new BlobServiceClient using the connection string var blobServiceClient = new BlobServiceClient(connectionString); // Create a new container var container = blobServiceClient.CreateBlobContainer(\"mycontainer\"); // Upload a file to the container using (var fileStream = File.OpenRead(\"path/to/file.txt\")) { container.UploadBlob(\"file.txt\", fileStream); } // Download the file from the container var downloadedBlob = container.GetBlobClient(\"file.txt\").Download(); using (var fileStream = File.OpenWrite(\"path/to/downloaded-file.txt\")) { downloadedBlob.Value.Content.CopyTo(fileStream); } } } Math Expressions This sentence uses $ delimiters to show math inline: \\(\\sqrt{3x-1}+(1+x)^2\\) The Cauchy-Schwarz Inequality \\(\\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)\\) This expression uses \\$ to display a dollar sign: \\(\\sqrt{\\$4}\\) To split $100 in half, we calculate \\(100/2\\) Custom Syntax Highlighting resource storageAccount 'Microsoft.Storage/storageAccounts@2021-06-01' = { name: 'hello' // (...) } Tabs Linux Windows Content for Linux... Content for Windows... The above tab group was created with the following syntax: # [Linux](#tab/linux) Content for Linux... # [Windows](#tab/windows) Content for Windows... --- Tabs are indicated by using a specific link syntax within a Markdown header. The syntax can be described as follows: # [Tab Display Name](#tab/tab-id) A tab starts with a Markdown header, #, and is followed by a Markdown link [](). The text of the link will become the text of the tab header, displayed to the customer. In order for the header to be recognized as a tab, the link itself must start with #tab/ and be followed by an ID representing the content of the tab. The ID is used to sync all same-ID tabs across the page. Using the above example, when a user selects a tab with the link #tab/windows, all tabs with the link #tab/windows on the page will be selected. Dependent tabs It's possible to make the selection in one set of tabs dependent on the selection in another set of tabs. Here's an example of that in action: .NET .NET TypeScript TypeScript REST API .NET content for Linux... .NET content for Windows... TypeScript content for Linux... TypeScript content for Windows... REST API content, independent of platform... flowchart LR A[Hard] -->|Text| B(Round) B --> C{Decision} C -->|One| D[Result 1] C -->|Two| E[Result 2] Notice how changing the Linux/Windows selection above changes the content in the .NET and TypeScript tabs. This is because the tab group defines two versions for each .NET and TypeScript, where the Windows/Linux selection above determines which version is shown for .NET/TypeScript. Here's the markup that shows how this is done: # [.NET](#tab/dotnet/linux) .NET content for Linux... # [.NET](#tab/dotnet/windows) .NET content for Windows... # [TypeScript](#tab/typescript/linux) TypeScript content for Linux... # [TypeScript](#tab/typescript/windows) TypeScript content for Windows... # [REST API](#tab/rest) REST API content, independent of platform... ---"
"keywords": "Markdown Markdown is a lightweight markup language with plain text formatting syntax. Docfx supports CommonMark compliant Markdown parsed through the Markdig parsing engine. Link to Math Expressions Block Quotes This is a block quote. Alerts Note Information the user should notice even if skimming. Tip Optional information to help a user be more successful. Important Essential information required for user success. Caution Negative potential consequences of an action. Warning Dangerous certain consequences of an action. MY TODO This is a TODO. Image Mermaid Diagrams Flowchart flowchart LR A[Hard] -->|Text| B(Round) B --> C{Decision} C -->|One| D[Result 1] C -->|Two| E[Result 2] Code Snippet The example highlights lines 2, line 5 to 7 and lines 9 to the end of the file. using System; using Azure; using Azure.Storage; using Azure.Storage.Blobs; class Program { static void Main(string[] args) { // Define the connection string for the storage account string connectionString = \"DefaultEndpointsProtocol=https;AccountName=<your-account-name>;AccountKey=<your-account-key>;EndpointSuffix=core.windows.net\"; // Create a new BlobServiceClient using the connection string var blobServiceClient = new BlobServiceClient(connectionString); // Create a new container var container = blobServiceClient.CreateBlobContainer(\"mycontainer\"); // Upload a file to the container using (var fileStream = File.OpenRead(\"path/to/file.txt\")) { container.UploadBlob(\"file.txt\", fileStream); } // Download the file from the container var downloadedBlob = container.GetBlobClient(\"file.txt\").Download(); using (var fileStream = File.OpenWrite(\"path/to/downloaded-file.txt\")) { downloadedBlob.Value.Content.CopyTo(fileStream); } } } Math Expressions This sentence uses $ delimiters to show math inline: \\(\\sqrt{3x-1}+(1+x)^2\\) The Cauchy-Schwarz Inequality \\(\\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)\\) This expression uses \\$ to display a dollar sign: \\(\\sqrt{\\$4}\\) To split $100 in half, we calculate \\(100/2\\) Custom Syntax Highlighting resource storageAccount 'Microsoft.Storage/storageAccounts@2021-06-01' = { name: 'hello' // (...) } Tabs Linux Windows Content for Linux... Content for Windows... The above tab group was created with the following syntax: # [Linux](#tab/linux) Content for Linux... # [Windows](#tab/windows) Content for Windows... --- Tabs are indicated by using a specific link syntax within a Markdown header. The syntax can be described as follows: # [Tab Display Name](#tab/tab-id) A tab starts with a Markdown header, #, and is followed by a Markdown link [](). The text of the link will become the text of the tab header, displayed to the customer. In order for the header to be recognized as a tab, the link itself must start with #tab/ and be followed by an ID representing the content of the tab. The ID is used to sync all same-ID tabs across the page. Using the above example, when a user selects a tab with the link #tab/windows, all tabs with the link #tab/windows on the page will be selected. Dependent tabs It's possible to make the selection in one set of tabs dependent on the selection in another set of tabs. Here's an example of that in action: .NET .NET TypeScript TypeScript REST API .NET content for Linux... .NET content for Windows... TypeScript content for Linux... TypeScript content for Windows... REST API content, independent of platform... flowchart LR A[Hard] -->|Text| B(Round) B --> C{Decision} C -->|One| D[Result 1] C -->|Two| E[Result 2] Notice how changing the Linux/Windows selection above changes the content in the .NET and TypeScript tabs. This is because the tab group defines two versions for each .NET and TypeScript, where the Windows/Linux selection above determines which version is shown for .NET/TypeScript. Here's the markup that shows how this is done: # [.NET](#tab/dotnet/linux) .NET content for Linux... # [.NET](#tab/dotnet/windows) .NET content for Windows... # [TypeScript](#tab/typescript/linux) TypeScript content for Linux... # [TypeScript](#tab/typescript/windows) TypeScript content for Windows... # [REST API](#tab/rest) REST API content, independent of platform... ---"
},
"index.html": {
"href": "index.html",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ <h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<h1 id="markdown">Markdown</h1>

<p><a href="https://daringfireball.net/projects/markdown/" target="_blank" rel="noopener noreferrer nofollow" class="external">Markdown</a> is a lightweight markup language with plain text formatting syntax. Docfx supports <a href="https://commonmark.org/" target="_blank" rel="noopener noreferrer nofollow" class="external">CommonMark</a> compliant Markdown parsed through the <a href="https://github.com/xoofx/markdig" target="_blank" rel="noopener noreferrer nofollow" class="external">Markdig</a> parsing engine.</p>
<p>Link to <a href="#math-expressions">Math Expressions</a></p>
<h1 id="block-quotes">Block Quotes</h1>
<blockquote>
<p>This is a block quote.</p>
Expand Down