Skip to content

Commit d643f62

Browse files
committed
fix: toc items orders issue
1 parent b089586 commit d643f62

File tree

5 files changed

+459
-8
lines changed

5 files changed

+459
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#nullable enable
5+
6+
namespace Docfx.Dotnet;
7+
8+
/// <summary>
9+
/// StringComparer that simulate <see cref="StringComparer.InvariantCulture"> behavior for ASCII chars.
10+
/// </summary>
11+
/// <remarks>
12+
/// .NET StringComparer ignores non-printable chars on string comparison
13+
/// (e.g. StringComparer.InvariantCulture.Compare("\x0000 ZZZ \x0000"," ZZZ ")) returns 0).
14+
/// This feature is not implement by this comparer.
15+
/// </remarks>
16+
internal sealed class SymbolStringComparer : IComparer<string>
17+
{
18+
public static readonly SymbolStringComparer Instance = new();
19+
20+
private SymbolStringComparer() { }
21+
22+
public int Compare(string? x, string? y)
23+
{
24+
if (ReferenceEquals(x, y)) return 0;
25+
if (x == null) return -1;
26+
if (y == null) return 1;
27+
28+
var xSpan = x.AsSpan();
29+
var ySpan = y.AsSpan();
30+
31+
int minLength = Math.Min(xSpan.Length, ySpan.Length);
32+
33+
int savedFirstDiff = 0;
34+
for (int i = 0; i < minLength; ++i)
35+
{
36+
var xChar = xSpan[i];
37+
var yChar = ySpan[i];
38+
39+
if (xChar == yChar)
40+
continue;
41+
42+
if (char.IsAscii(xChar) && char.IsAscii(yChar))
43+
{
44+
// Gets custom char order
45+
var xOrder = AsciiCharSortOrders[xChar];
46+
var yOrder = AsciiCharSortOrders[yChar];
47+
48+
var result = xOrder.CompareTo(yOrder);
49+
if (result == 0)
50+
continue;
51+
52+
// Custom order logics for method parameters.
53+
if ((xChar == ',' && yChar == ')') || (xChar == ')' && yChar == ','))
54+
return -result; // Returns result with inverse sign.
55+
56+
// Save first char case comparison result. (To simulate `StringComparer.InvariantCulture` behavior).
57+
if (char.ToUpper(xChar) == char.ToUpper(yChar))
58+
{
59+
if (savedFirstDiff == 0)
60+
savedFirstDiff = result;
61+
continue;
62+
}
63+
64+
return result;
65+
}
66+
else
67+
{
68+
// Compare non-ASCII char with ordinal order
69+
int result = xChar.CompareTo(yChar);
70+
if (result != 0)
71+
return result;
72+
}
73+
}
74+
75+
// Return saved result if case difference exists and string length is same.
76+
if (savedFirstDiff != 0 && x.Length == y.Length)
77+
return savedFirstDiff;
78+
79+
// Otherwise compare text length.
80+
return x.Length.CompareTo(y.Length);
81+
}
82+
83+
// ASCII character order lookup table.
84+
// This table is based on StringComparer.InvariantCulture's charactor sort order.
85+
private static readonly byte[] AsciiCharSortOrders =
86+
[
87+
0, // NUL
88+
0, // SOH
89+
0, // STX
90+
0, // ETX
91+
0, // EOT
92+
0, // ENQ
93+
0, // ACK
94+
0, // BEL
95+
0, // BS
96+
28, // HT (Horizontal Tab)
97+
29, // LF (Line Feed)
98+
30, // VT (Vertical Tab)
99+
31, // FF (Form Feed)
100+
32, // CR (Carriage Return)
101+
0, // SO
102+
0, // SI
103+
0, // DLE
104+
0, // DC1
105+
0, // DC2
106+
0, // DC3
107+
0, // DC4
108+
0, // NAK
109+
0, // SYN
110+
0, // ETB
111+
0, // CAN
112+
0, // EM
113+
0, // SUB
114+
0, // ESC
115+
0, // FS
116+
0, // GS
117+
0, // RS
118+
0, // US
119+
33, // SP (Space)
120+
39, // !
121+
43, // "
122+
55, // #
123+
65, // $
124+
56, // %
125+
54, // &
126+
42, // '
127+
44, // (
128+
45, // )
129+
51, // *
130+
59, // +
131+
36, // ,
132+
35, // -
133+
41, // .
134+
52, // /
135+
66, // 0
136+
67, // 1
137+
68, // 2
138+
69, // 3
139+
70, // 4
140+
71, // 5
141+
72, // 6
142+
73, // 7
143+
74, // 8
144+
75, // 9
145+
38, // :
146+
37, // ;
147+
60, // <
148+
61, // =
149+
62, // >
150+
40, // ?
151+
50, // @
152+
77, // A
153+
79, // B
154+
81, // C
155+
83, // D
156+
85, // E
157+
87, // F
158+
89, // G
159+
91, // H
160+
93, // I
161+
95, // J
162+
97, // K
163+
99, // L
164+
101, // M
165+
103, // N
166+
105, // O
167+
107, // P
168+
109, // Q
169+
111, // R
170+
113, // S
171+
115, // T
172+
117, // U
173+
119, // V
174+
121, // W
175+
123, // X
176+
125, // Y
177+
127, // Z
178+
46, // [
179+
53, // \
180+
47, // ]
181+
58, // ^
182+
34, // _
183+
57, // `
184+
76, // a
185+
78, // b
186+
80, // c
187+
82, // d
188+
84, // e
189+
86, // f
190+
88, // g
191+
90, // h
192+
92, // i
193+
94, // j
194+
96, // k
195+
98, // l
196+
100, // m
197+
102, // n
198+
104, // o
199+
106, // p
200+
108, // q
201+
110, // r
202+
112, // s
203+
114, // t
204+
116, // u
205+
118, // v
206+
120, // w
207+
122, // x
208+
124, // y
209+
126, // z
210+
48, // {
211+
63, // |
212+
49, // }
213+
64, // ~
214+
0, // ESC
215+
];
216+
}

src/Docfx.Dotnet/YamlViewModelExtensions.cs

+8-8
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,13 @@ public static List<TocItemViewModel> ToTocViewModel(this MetadataItem item, stri
115115
case MemberType.Toc:
116116
case MemberType.Namespace:
117117
var result = new List<TocItemViewModel>();
118-
foreach (var child in item.Items
118+
119+
return item.Items
119120
.OrderBy(x => x.Type == MemberType.Namespace ? 0 : 1)
120-
.ThenBy(x => x.Name)
121-
)
122-
{
123-
result.Add(child.ToTocItemViewModel(parentNamespace));
124-
}
125-
return result;
121+
.ThenBy(x => x.Name, SymbolStringComparer.Instance)
122+
.Select(x => x.ToTocItemViewModel(parentNamespace))
123+
.ToList();
124+
126125
default:
127126
return null;
128127
}
@@ -239,7 +238,8 @@ public static ItemViewModel ToItemViewModel(this MetadataItem model, ExtractMeta
239238

240239
var children = model.Type is MemberType.Enum && config.EnumSortOrder is EnumSortOrder.DeclaringOrder
241240
? model.Items?.Select(x => x.Name).ToList()
242-
: model.Items?.Select(x => x.Name).OrderBy(s => s, StringComparer.Ordinal).ToList();
241+
: model.Items?.Select(x => x.Name).OrderBy(s => s, SymbolStringComparer.Instance).ToList();
242+
243243

244244
var result = new ItemViewModel
245245
{

test/Docfx.Dotnet.Tests/Docfx.Dotnet.Tests.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<!-- Use following setting when running tests withoug Globalization Invariant Mode. -->
5+
<!--<InvariantGlobalization>false</InvariantGlobalization>-->
6+
</PropertyGroup>
7+
28
<ItemGroup>
39
<None Include="TestDataReferences\**" CopyToOutputDirectory="PreserveNewest" />
410
</ItemGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Xunit;
5+
6+
#nullable enable
7+
8+
namespace Docfx.Dotnet.Tests;
9+
10+
public partial class SymbolStringComparerTest
11+
{
12+
private static class TestData
13+
{
14+
/// <summary>
15+
/// Test data for string array order tests.
16+
/// </summary>
17+
public static TheoryData<string[]> StringArrays =>
18+
[
19+
// Contains underscore
20+
[
21+
"__",
22+
"__a",
23+
"_1",
24+
"1_",
25+
"a_a",
26+
"A_a",
27+
"a_aa",
28+
"a_ab",
29+
"aaa"
30+
],
31+
// Case differences
32+
[
33+
"aaa",
34+
"AAA",
35+
"AAA<ABC>",
36+
"AAAA",
37+
"aaab",
38+
],
39+
// Mixed generics
40+
[
41+
"IRoutedView",
42+
"IRoutedView_`1",
43+
"IRoutedView`1",
44+
"IRoutedView<TViewModel>",
45+
"IRoutedView1",
46+
"IRoutedViewModel",
47+
"Null(object? obj)",
48+
"Null<T>(T obj)",
49+
"NullOrEmpty(string? text)",
50+
],
51+
];
52+
}
53+
}

0 commit comments

Comments
 (0)