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
7 changes: 7 additions & 0 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ jobs:
cd wiki
mdbook build

# mdBook has no per-page description and cannot build a page URL in its template,
# so descriptions declared as <!-- description: ... --> comments are lifted into
# <head> here, along with og:url and canonical.
- name: Add Page Metadata
shell: pwsh
run: ./wiki/tools/Add-PageMetadata.ps1 -Book ./wiki/book -Require

# mdBook rewrites .md links to .html without verifying the target exists, so a
# renamed or removed page builds cleanly and 404s in production. This also checks
# that the legacy wiki redirect map still resolves.
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<VersionPrefix>10.2.1</VersionPrefix>
<VersionPrefix>10.2.2</VersionPrefix>
<AssemblyVersion>10.2.0.0</AssemblyVersion>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<RootNamespace>Asp.Versioning.OpenApi</RootNamespace>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Bump patched version due to transitive dependency
Fixed XML Comment whitespace handling [Issue #1205](https://github.com/dotnet/aspnet-api-versioning/issues/1205)
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ public class XmlComments
/// Initializes a new instance of the <see cref="XmlComments"/> class.
/// </summary>
/// <param name="path">The file path of the XML comments to read.</param>
protected XmlComments( string path ) => Xml = File.Exists( path ) ? XDocument.Load( path ) : new();
/// <remarks>The whitespace of the source file is preserved. A reader discards a text node that is only
/// whitespace by default, which is not insignificant here: it is the indentation the file is written with,
/// and it is the only way to know the margin that has to be removed before the text reads as Markdown. It is
/// also the space between two adjacent tags, which is the space between the words they wrap.</remarks>
protected XmlComments( string path ) =>
Xml = File.Exists( path ) ? XDocument.Load( path, LoadOptions.PreserveWhitespace ) : new();

/// <summary>
/// Creates and returns new <see cref="XmlComments"/> from the specified file.
Expand Down Expand Up @@ -374,7 +379,12 @@ private static void ResolveParamRefTags( XElement element )
// containing element is rebuilt rather than each <para /> being replaced in turn, because the whitespace
// between two of them is not a reliable separator: XLinq merges the text nodes around a replaced element.
//
// This runs last, so every other tag has already been resolved to text and <para /> is the only element left.
// Only a <para /> starts a paragraph. Everything between two of them belongs to the same one, including a tag
// left in the tree because it has no Markdown of its own, such as <see /> or <u />. Reading one of those as a
// block of its own would break the sentence around it into a paragraph per fragment.
//
// This runs last, so every other tag has already been resolved to text and <para /> is the only element left
// that carries structure.
private static void ResolveParaTags( XElement element )
{
foreach ( var parent in element.DescendantsAndSelf().ToArray() )
Expand All @@ -385,26 +395,41 @@ private static void ResolveParaTags( XElement element )
}

var blocks = new List<string>();
var paragraph = new StringBuilder();

foreach ( var node in parent.Nodes() )
{
var text = node switch
if ( node is XElement para && para.Name == "para" )
{
XText content => TrimEachLine( content.Value ),
XElement para => TrimEachLine( para.Value ),
_ => string.Empty,
};

if ( text.Length > 0 )
AddBlock( blocks, paragraph.ToString() );
paragraph.Clear();
AddBlock( blocks, para.Value );
}
else if ( node is XText content )
{
blocks.Add( text );
paragraph.Append( content.Value );
}
else if ( node is XElement other )
{
paragraph.Append( other.Value );
}
}

AddBlock( blocks, paragraph.ToString() );
parent.ReplaceNodes( new XText( string.Join( "\n\n", blocks ) ) );
}
}

private static void AddBlock( List<string> blocks, string text )
{
var block = TrimEachLine( text );

if ( block.Length > 0 )
{
blocks.Add( block );
}
}

private static void ResolveListTags( XElement element )
{
foreach ( var list in element.Descendants( "list" ).ToArray() )
Expand Down Expand Up @@ -579,14 +604,19 @@ private static void ResolveInlineCode( XElement element )
// <b>, <i>, and <a> are the html tags a documentation comment carries inline, and each has a direct markdown
// equivalent. rewriting them keeps the meaning that reading the text of the enclosing element would drop.
// the tags are visited from the inside out so that one nested in another is rewritten before it is absorbed.
//
// emphasis is delimited by an asterisk rather than an underscore. the two are interchangeable on their own,
// but an underscore only opens or closes emphasis at a word boundary, so it is literal text in the middle of
// a word and it does not pair with the asterisk of a <b> nested the other way around; <b><i>x</i></b> and
// <i><b>x</b></i> then render differently despite meaning the same thing.
private static void ResolveInlineTags( XElement element )
{
foreach ( var inline in element.Descendants().Reverse().ToArray().Where( e => e.Parent is not null ) )
{
var text = inline.Name.LocalName switch
{
"b" => Delimit( inline.Value, "**" ),
"i" => Delimit( inline.Value, "_" ),
"i" => Delimit( inline.Value, "*" ),
"a" => LinkOf( inline ),
_ => default,
};
Expand Down Expand Up @@ -699,7 +729,10 @@ private static int MarginOf( string text )
var lines = text.Split( '\n' );
var margin = int.MaxValue;

for ( var i = 0; i < lines.Length; i++ )
// the text of a member begins immediately after its opening tag rather than at the start of a line, so
// whatever precedes the first break carries none of the indentation the margin is measured from. counting
// it would report a margin of zero and leave the indentation on every line that does start one.
for ( var i = 1; i < lines.Length; i++ )
{
var line = lines[i];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,34 @@ public class Documented
/// </summary>
public string Emphasis { get; set; }

/// <summary>
/// Gets or sets the sibling, which is <u>underlined</u> in place.
/// <para>A note.</para>
/// </summary>
public string Sibling { get; set; }

/// <summary>
/// Gets or sets the adjacent.
/// <para>
/// <b>Remark <i>of</i></b> <u>GetToDo</u>
/// </para>
/// <para>
/// <b>Remark</b> <i>of</i> <u>GetToDo</u>
/// </para>
/// </summary>
public string Adjacent { get; set; }

/// <summary>
/// Gets or sets the intraword.
/// <para>
/// Very<b><i>long</i></b>word
/// </para>
/// <para>
/// Very<i><b>long</b></i>word
/// </para>
/// </summary>
public string Intraword { get; set; }

/// <summary>
/// Gets or sets the reference, which is described by <a href="https://example.com">the
/// specification</a>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,34 @@ public static class MinimalApi
/// <returns>The detailed answer.</returns>
public static int Detailed() => 42;

/// <summary>Mixed</summary>
/// <remarks>
/// Text before code
///
/// <code>
/// var index = 5;
/// index++;
/// </code>
///
/// Text after code
/// </remarks>
/// <returns>The mixed answer.</returns>
public static int Mixed() => 42;

/// <summary>Outlined</summary>
/// <remarks>
/// Text before list
///
/// <list type="bullet">
/// <item>First</item>
/// <item>Second</item>
/// </list>
///
/// Text after list
/// </remarks>
/// <returns>The outlined answer.</returns>
public static int Outlined() => 42;

/// <summary>
/// Echo
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ public void bold_and_italic_should_be_resolved_into_emphasis()
var summary = comments.GetSummary( property );

// assert
summary.Should().Be( "Gets or sets the highlights, which are **important** and _subtle_." );
summary.Should().Be( "Gets or sets the highlights, which are **important** and *subtle*." );
}

[Fact]
Expand All @@ -274,7 +274,62 @@ public void nested_emphasis_should_be_resolved_from_the_inside_out()
var summary = comments.GetSummary( property );

// assert
summary.Should().Be( "Gets or sets the emphasis, which is **very _strongly_ worded**." );
summary.Should().Be( "Gets or sets the emphasis, which is **very *strongly* worded**." );
}

[Fact]
public void nested_emphasis_should_be_resolved_the_same_way_in_either_order()
{
// arrange
var comments = XmlComments.FromFile( FilePath.XmlCommentFile );
var property = typeof( Documented ).GetProperty( nameof( Documented.Intraword ) );

// act
var summary = comments.GetSummary( property );

// assert
summary.Should().Be(
"Gets or sets the intraword.\n" +
"\n" +
"Very***long***word\n" +
"\n" +
"Very***long***word" );
}

[Fact]
public void tag_beside_a_paragraph_should_not_start_one()
{
// arrange
var comments = XmlComments.FromFile( FilePath.XmlCommentFile );
var property = typeof( Documented ).GetProperty( nameof( Documented.Sibling ) );

// act
var summary = comments.GetSummary( property );

// assert
summary.Should().Be(
"Gets or sets the sibling, which is underlined in place.\n" +
"\n" +
"A note." );
}

[Fact]
public void space_between_adjacent_inline_tags_should_be_retained()
{
// arrange
var comments = XmlComments.FromFile( FilePath.XmlCommentFile );
var property = typeof( Documented ).GetProperty( nameof( Documented.Adjacent ) );

// act
var summary = comments.GetSummary( property );

// assert
summary.Should().Be(
"Gets or sets the adjacent.\n" +
"\n" +
"**Remark *of*** GetToDo\n" +
"\n" +
"**Remark** *of* GetToDo" );
}

[Fact]
Expand Down Expand Up @@ -373,6 +428,61 @@ public async Task paramref_should_be_resolved_in_the_document()
description.GetValue<string>().Should().Be( "The value of `id`." );
}

[Fact]
public void text_around_a_code_block_should_not_be_indented()
{
// arrange
var comments = XmlComments.FromFile( FilePath.XmlCommentFile );
var method = typeof( MinimalApi ).GetMethod( nameof( MinimalApi.Mixed ) );

// act
var remarks = comments.GetRemarks( method );

// assert
remarks.Should().Be(
"Text before code\n" +
"\n\n" +
"```\n" +
"var index = 5;\n" +
"index++;\n" +
"```\n" +
"\n\n" +
"Text after code" );
}

[Fact]
public void text_around_a_list_should_not_be_indented()
{
// arrange
var comments = XmlComments.FromFile( FilePath.XmlCommentFile );
var method = typeof( MinimalApi ).GetMethod( nameof( MinimalApi.Outlined ) );

// act
var remarks = comments.GetRemarks( method );

// assert
remarks.Should().Be(
"Text before list\n" +
"\n" +
"* First\n" +
"* Second\n" +
"\n" +
"Text after list" );
}

[Fact]
public async Task text_around_a_code_block_should_not_be_indented_in_the_document()
{
// arrange
var paths = await GeneratePathsAsync();

// act
var description = paths["/test/mixed"]["get"]["description"].GetValue<string>();

// assert
description.Should().NotContain( "\n " ).And.StartWith( "Text before code" );
}

[Fact]
public async Task summary_and_value_should_describe_a_property()
{
Expand Down Expand Up @@ -421,6 +531,7 @@ private static async Task<JsonNode> GenerateDocumentAsync()
api.MapGet( "detailed", MinimalApi.Detailed );
api.MapGet( "documented", () => new Documented() );
api.MapGet( "echo/{id:int}", MinimalApi.Echo );
api.MapGet( "mixed", MinimalApi.Mixed );
app.MapOpenApi().WithDocumentPerVersion();

var cancellationToken = TestContext.Current.CancellationToken;
Expand Down
28 changes: 28 additions & 0 deletions wiki/src/404.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<div class="not-found">

<p class="not-found-code" role="img" aria-label="404"><span>4</span><span class="not-found-logo"></span><span>4</span></p>

# Not Found

Slithered through every page and API version. This page isn't in any of them.

```http
HTTP/2 404
api-supported-versions: 1.0
content-type: application/problem+json
content-length: 163

{
"type": "https://docs.api-versioning.org/problems#unsupported",
"title": "Not Found",
"status": 404,
"detail": "No page matched that URL in any API version.",
"code": "UnsupportedApiVersion"
}
```

It may have been renamed, sunset without a deprecation policy, or never shipped at all.
Try the [Introduction](index.html) or [Getting Started](getting-started.md), or press
<kbd>s</kbd> to search.

</div>
2 changes: 2 additions & 0 deletions wiki/src/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!-- description: Add API versioning to ASP.NET Core and ASP.NET Web API services. -->

# Introduction

Versioning is an important aspect of any mature web service. Microsoft has published REST API guidelines that require
Expand Down
Loading
Loading