diff --git a/src/Docfx.Build/TableOfContents/TocHelper.cs b/src/Docfx.Build/TableOfContents/TocHelper.cs index 5ff08517a92..6523a71944e 100644 --- a/src/Docfx.Build/TableOfContents/TocHelper.cs +++ b/src/Docfx.Build/TableOfContents/TocHelper.cs @@ -6,15 +6,14 @@ using Docfx.Common; using Docfx.DataContracts.Common; using Docfx.Plugins; +using YamlDotNet.Core.Events; +using YamlDotNet.Core; +using Constants = Docfx.DataContracts.Common.Constants; namespace Docfx.Build.TableOfContents; public static class TocHelper { - private static readonly YamlDeserializerWithFallback _deserializer = - YamlDeserializerWithFallback.Create>() - .WithFallback(); - internal static List ResolveToc(ImmutableList models) { var tocCache = new Dictionary(FilePathComparer.OSPlatformSensitiveStringComparer); @@ -63,21 +62,20 @@ public static TocItemViewModel LoadSingleToc(string file) var fileType = Utility.GetTocFileType(file); try { - if (fileType == TocFileType.Markdown) - { - return new() - { - Items = MarkdownTocReader.LoadToc(EnvironmentContext.FileAbstractLayer.ReadAllText(file), file) - }; - } - else if (fileType == TocFileType.Yaml) + switch (fileType) { - return _deserializer.Deserialize(file) switch - { - List vm => new() { Items = vm }, - TocItemViewModel root => root, - _ => throw new NotSupportedException($"{file} is not a valid TOC file."), - }; + case TocFileType.Markdown: + return new() + { + Items = MarkdownTocReader.LoadToc(EnvironmentContext.FileAbstractLayer.ReadAllText(file), file) + }; + case TocFileType.Yaml: + { + var yaml = EnvironmentContext.FileAbstractLayer.ReadAllText(file); + return DeserializeYamlToc(yaml); + } + default: + throw new NotSupportedException($"{file} is not a valid TOC file, supported TOC files should be either \"{Constants.TableOfContents.MarkdownTocFileName}\" or \"{Constants.TableOfContents.YamlTocFileName}\"."); } } catch (Exception e) @@ -86,7 +84,18 @@ public static TocItemViewModel LoadSingleToc(string file) Logger.LogError(message, code: ErrorCodes.Toc.InvalidTocFile); throw new DocumentException(message, e); } + } + + private static TocItemViewModel DeserializeYamlToc(string yaml) + { + // Parse yaml content to determine TOC type (`List` or TocItemViewModel). + var parser = new Parser(new Scanner(new StringReader(yaml), skipComments: true)); + bool isListItems = parser.TryConsume(out var _) + && parser.TryConsume(out var _) + && parser.TryConsume(out var _); - throw new NotSupportedException($"{file} is not a valid TOC file, supported TOC files should be either \"{Constants.TableOfContents.MarkdownTocFileName}\" or \"{Constants.TableOfContents.YamlTocFileName}\"."); + return isListItems + ? new TocItemViewModel { Items = YamlUtility.Deserialize>(new StringReader(yaml)) } + : YamlUtility.Deserialize(new StringReader(yaml)); } } diff --git a/src/Docfx.Common/YamlDeserializerWithFallback.cs b/src/Docfx.Common/YamlDeserializerWithFallback.cs deleted file mode 100644 index ac34aaeab9b..00000000000 --- a/src/Docfx.Common/YamlDeserializerWithFallback.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using YamlDotNet.Core; - -namespace Docfx.Common; - -public class YamlDeserializerWithFallback -{ - private readonly Func, object> _textReaderDeserialize; - private readonly Func _filePathDeserialize; - - private YamlDeserializerWithFallback( - Func, object> textReaderDeserialize, - Func filePathDeserialize) - { - _textReaderDeserialize = textReaderDeserialize; - _filePathDeserialize = filePathDeserialize; - } - - public static YamlDeserializerWithFallback Create() => - new( - (Func tr) => YamlUtility.Deserialize(tr()), - (string path) => YamlUtility.Deserialize(path)); - - public YamlDeserializerWithFallback WithFallback() => - new( - Fallback(_textReaderDeserialize, tr => YamlUtility.Deserialize(tr())), - Fallback(_filePathDeserialize, p => YamlUtility.Deserialize(p))); - - public object Deserialize(Func reader) => - _textReaderDeserialize(reader); - - public object Deserialize(string filePath) => - _filePathDeserialize(filePath); - - private static Func Fallback( - Func first, - Func second) => - p => - { - try - { - return first(p); - } - catch (YamlException ex) - { - try - { - return second(p); - } - catch (YamlException exFallback) - { - if (ex.Start.CompareTo(exFallback.Start) < 0) - { - throw; - } - } - throw; - } - }; -} diff --git a/test/Docfx.Build.Tests/TocHelperTest.cs b/test/Docfx.Build.Tests/TocHelperTest.cs new file mode 100644 index 00000000000..f7a4d01fb5f --- /dev/null +++ b/test/Docfx.Build.Tests/TocHelperTest.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Docfx.Common; +using Docfx.DataContracts.Common; +using FluentAssertions; +using Xunit; + +namespace Docfx.Build.TableOfContents.Tests; + +public class TocHelperTest +{ + [Fact] + public void TestItemDeserialization() + { + // Arrange + var item = new TocItemViewModel + { + Items = + [ + new TocItemViewModel { Uid = "item1" }, + new TocItemViewModel { Uid = "item2" } + ], + }; + + var yaml = ToYaml(item); + var filePath = Path.Combine(Path.GetTempPath(), "toc.yml"); + File.WriteAllText(filePath, yaml, new UTF8Encoding(false)); + + try + { + // Act + var result = TocHelper.LoadSingleToc(filePath); + + // Assert + result.Should().BeEquivalentTo(item); + } + finally + { + File.Delete(filePath); + } + } + + [Fact] + public void TestListDeserialization() + { + // Arrange + var items = new TocItemViewModel[] + { + new TocItemViewModel { Uid = "item1" }, + new TocItemViewModel { Uid = "item2" }, + }; + + var yaml = ToYaml(items); + var filePath = Path.Combine(Path.GetTempPath(), "toc.yml"); + File.WriteAllText(filePath, yaml); + + try + { + // Act + var result = TocHelper.LoadSingleToc(filePath); + + // Assert + result.Uid.Should().BeNull(); + result.Href.Should().BeNull(); + result.Items.Should().BeEquivalentTo(items); + } + finally + { + File.Delete(filePath); + } + } + + [Fact] + public void TestItemDeserializationWithEncoding() + { + // Arrange + var item = new TocItemViewModel + { + Items = + [ + new TocItemViewModel { Uid = "item1" }, + new TocItemViewModel { Uid = "item2" } + ], + }; + + var yaml = ToYaml(item); + + foreach (var encoding in Encodings) + { + var filePath = Path.Combine(Path.GetTempPath(), "toc.yml"); + File.WriteAllText(filePath, yaml, encoding); + + try + { + // Act + var result = TocHelper.LoadSingleToc(filePath); + + // Assert + result.Should().BeEquivalentTo(item); + } + finally + { + File.Delete(filePath); + } + } + } + + private static readonly Encoding[] Encodings = + [ + new UTF8Encoding(false), + new UTF8Encoding(true), + Encoding.Unicode, + Encoding.BigEndianUnicode, + ]; + + private static string ToYaml(T model) + { + using StringWriter sw = new StringWriter(); + YamlUtility.Serialize(sw, model); + return sw.ToString(); + } +} diff --git a/test/Docfx.Common.Tests/YamlDeserializerWithFallbackTest.cs b/test/Docfx.Common.Tests/YamlDeserializerWithFallbackTest.cs deleted file mode 100644 index 4674cdf5943..00000000000 --- a/test/Docfx.Common.Tests/YamlDeserializerWithFallbackTest.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; -using YamlDotNet.Core; - -namespace Docfx.Common.Tests; - -public class YamlDeserializerWithFallbackTest -{ - [Fact] - public void TestYamlDeserializerWithFallback() - { - var deserializer = YamlDeserializerWithFallback.Create() - .WithFallback>(); - { - var obj = deserializer.Deserialize(() => new StringReader(@"A")); - Assert.NotNull(obj); - var a = Assert.IsType(obj); - Assert.Equal("A", a); - } - { - var obj = deserializer.Deserialize(() => new StringReader(@"- A -- B")); - Assert.NotNull(obj); - var a = Assert.IsType>(obj); - Assert.Equal("A", a[0]); - Assert.Equal("B", a[1]); - } - { - var ex = Assert.Throws(() => deserializer.Deserialize(() => new StringReader(@"- A -- A: abc"))); - Assert.Equal(2, ex.Start.Line); - Assert.Equal(3, ex.Start.Column); - } - } - - [Fact] - public void TestYamlDeserializerWithFallback_MultiFallback() - { - var deserializer = YamlDeserializerWithFallback.Create() - .WithFallback() - .WithFallback(); - { - var obj = deserializer.Deserialize(() => new StringReader(@"1")); - Assert.NotNull(obj); - var a = Assert.IsType(obj); - Assert.Equal(1, a); - } - { - var obj = deserializer.Deserialize(() => new StringReader(@"A")); - Assert.NotNull(obj); - var a = Assert.IsType(obj); - Assert.Equal("A", a); - } - { - var obj = deserializer.Deserialize(() => new StringReader(@"- A -- B")); - Assert.NotNull(obj); - var a = Assert.IsType(obj); - Assert.Equal("A", a[0]); - Assert.Equal("B", a[1]); - } - { - var ex = Assert.Throws(() => deserializer.Deserialize(() => new StringReader(@"- A -- A: abc"))); - Assert.Equal(2, ex.Start.Line); - Assert.Equal(3, ex.Start.Column); - } - } -} diff --git a/test/docfx.Tests/Api.verified.cs b/test/docfx.Tests/Api.verified.cs index 978bdf5a41f..4f47a5faf89 100644 --- a/test/docfx.Tests/Api.verified.cs +++ b/test/docfx.Tests/Api.verified.cs @@ -2123,13 +2123,6 @@ public static class XrefUtility { public static bool TryGetXrefStringValue(this Docfx.Plugins.XRefSpec spec, string key, out string value) { } } - public class YamlDeserializerWithFallback - { - public object Deserialize(System.Func reader) { } - public object Deserialize(string filePath) { } - public Docfx.Common.YamlDeserializerWithFallback WithFallback() { } - public static Docfx.Common.YamlDeserializerWithFallback Create() { } - } public static class YamlMime { public const string ManagedReference = "YamlMime:ManagedReference";