diff --git a/Directory.Build.props b/Directory.Build.props
index e947a2a..30b1cab 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,9 @@
1.0.0
- The Docfx plugin to generate documentation from xml-based files via intermediate XSLT transformation into Markdown.
+
+ The Docfx plugin to generate documentation from xml-based files via intermediate template transformations into Markdown.
+
Docfx plugin XSD XML Markdown documentation
Heleonix - Hennadii Lutsyshyn
@@ -26,7 +28,7 @@
enable
latest
true
- True
+ true
en-US
en-US
diff --git a/README.md b/README.md
index b8b90d8..62bef81 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,16 @@
[![Release: .NET / NuGet](https://github.com/Heleonix/Heleonix.Docfx.Plugins.XmlDoc/actions/workflows/release-net-nuget.yml/badge.svg)](https://github.com/Heleonix/Heleonix.Docfx.Plugins.XmlDoc/actions/workflows/release-net-nuget.yml)
-The Docfx plugin to generate documentation from xml-based files via intermediate XSLT transformation into Markdown.
+The Docfx plugin to generate documentation from xml-based files via intermediate template transformations into Markdown.
## Install
https://www.nuget.org/packages/Heleonix.Docfx.Plugins.XmlDoc
+## Documentation
+
+See [Heleonix.Docfx.Plugins.XmlDoc](https://heleonix.github.io/docs/Plugins/Heleonix.Docfx.Plugins.XmlDoc.html)
+
## Usage
1. Install the plugin and make it accessible for `Docfx` as a custom template. See [How to enable plugins](https://dotnet.github.io/docfx/tutorial/howto_build_your_own_type_of_documentation_with_custom_plug-in.html#enable-plug-in)
@@ -19,11 +23,11 @@ https://www.nuget.org/packages/Heleonix.Docfx.Plugins.XmlDoc
}
```
By default, `.xml` and `.xsd` file formats are recognized.
-3. Configure the `docfx.json` with the plugin features. See the [Example].
+3. Configure the `docfx.json` with the plugin features. See the [docfx.json](#docfx.json).
-### Example
+### Examples
-Example of a configuration in a simple `docfx.json` file:
+#### docfx.json
```json
{
@@ -52,7 +56,7 @@ Example of a configuration in a simple `docfx.json` file:
"templates/template-with-xmldoc-plugin"
],
"fileMetadata": {
- "hx.xmldoc.xslt": { "**.xsd": "./xml-to-md.xslt" },
+ "hx.xmldoc.template": { "**.xsd": "./xml-to-md.xslt" },
"hx.xmldoc.store": { "../../some-external-location/*.xsd": "internal-store-folder" }
"hx.xmldoc.toc": {
"**/*-some.xsd": { "action": "InsertAfter", "key": "~/articles/introduction.md" },
@@ -63,14 +67,122 @@ Example of a configuration in a simple `docfx.json` file:
}
```
+#### input xml-based file, i.e. Hx_NetBuild.xsd
+
+```xml
+
+
+
+
+
+ A path to the NetBuild artifacts directory.
+
+
+
+
+ A path to the solution file to build. Default is a .sln file found in the $Hx_WS_Dir.
+
+
+
+
+ The file with public/private keys pair to sign assemblies.
+
+
+
+
+```
+#### xslt template
+
+```xml
+
+
+
+
+
+
+#
+### Properties
+
+####
+
+
+
+
+
+
+
+```
+
+#### cshtml template
+
+```html
+@using System
+@using System.IO
+@using System.Xml.Linq
+@inherits RazorEngineCore.RazorEngineTemplateBase
+@{
+ XDocument model = Model;
+ XNamespace xs = "http://www.w3.org/2001/XMLSchema";
+
+ var fileName = Path.GetFileNameWithoutExtension(model.Document.BaseUri);
+ var elements = model.Document.Element(xs + "schema").Elements(xs + "element");
+ var props = elements.Where(e => e.Attribute("substitutionGroup")?.Value == "msb:Property");
+}
+---
+uid: @fileName
+---
+
+# @fileName
+
+@if (props.Count() > 0)
+{
+ ## Properties
+ @:
+ foreach (var prop in props)
+ {
+ #### @prop.Attribute("name").Value
+ @:
+ @prop.Element(xs + "annotation").Element(xs + "documentation").Value.Trim()
+ @:
+ }
+}
+```
+
+#### markdown output
+
+```markdown
+# Hx_NetBuild
+### Properties
+
+#### Hx_NetBuild_ArtifactsDir
+
+A path to the NetBuild artifacts directory.
+
+#### Hx_NetBuild_SlnFile
+
+A path to the solution file to build. Default is a .sln file found in the $Hx_WS_Dir.
+
+#### Hx_NetBuild_SnkFile
+
+The file with public/private keys pair to sign assemblies.
+```
+
### File Metadata
-`hx.xmldoc.xslt` - path to XSLT file to convert xml-based file to Markdown, which is then converted into output HTML by Docfx.
+`hx.xmldoc.template` - path to a template `.cshtml` or `.xslt` file to transform xml-based file to Markdown, which is then converted into output HTML by Docfx.
`hx.xmldoc.store` - a folder inside your documentation proejct, where the corresponding xml-based files are copied to
-and then used as source files to generate output HTML from.
-This is useful, when original files are not always available.
-It works like metadata files generated from .NET projects/dlls/xml documentation.
+and then used as source files to transform to intermediate markdown and generate output HTML from.
+This is useful, when original files are not always available, i.e. when your single documentation project is applied
+to different dotnet projects simultaneously to support multi-project documentation.
+It works like metadata files generated from .NET projects/dlls/xml documentation, where generated `yaml` metadata could
+be commited as part of your documentation project for future re-builds, when the original .NET project is not available.
Hrefs to such files can be specified as `internal-store-folder/your-file.xsd`.
`hx.xmldoc.toc` - specifies where and how the your xml-based files should be added into Table Of Contents.
@@ -88,11 +200,9 @@ Hrefs to such files can be specified as `internal-store-folder/your-file.xsd`.
You can watch the progress in the [PR: .NET](https://github.com/Heleonix/Heleonix.Docfx.Plugins.XmlDoc/actions/workflows/pr-net.yml) GitHub workflows
4. [Request review](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) from the code owner
5. Once approved, merge your Pull Request via [Squash and merge](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits)
-
> **IMPORTANT**
> While merging, enter a [Conventional Commits](https://www.conventionalcommits.org/) commit message.
> This commit message will be used in automatically generated [Github Release Notes](https://github.com/Heleonix/Heleonix.Docfx.Plugins.XmlDoc/releases)
> and [NuGet Release Notes](https://www.nuget.org/packages/Heleonix.Docfx.Plugins.XmlDoc/#releasenotes-body-tab)
-
6. Monitor the [Release: .NET / NuGet](https://github.com/Heleonix/Heleonix.Docfx.Plugins.XmlDoc/actions/workflows/release-net-nuget.yml) GitHub workflow to make sure your changes are delivered successfully
7. In case of any issues, please contact [heleonix.sln@gmail.com](mailto:heleonix.sln@gmail.com)
\ No newline at end of file
diff --git a/src/Heleonix.Docfx.Plugins.XmlDoc/FileMetadata.cs b/src/Heleonix.Docfx.Plugins.XmlDoc/FileMetadata.cs
index 0e9aab1..b249ce4 100644
--- a/src/Heleonix.Docfx.Plugins.XmlDoc/FileMetadata.cs
+++ b/src/Heleonix.Docfx.Plugins.XmlDoc/FileMetadata.cs
@@ -10,46 +10,6 @@ namespace Heleonix.Docfx.Plugins.XmlDoc;
///
/// Represents the model of the supported fileMetadata specified for content files to be processed by this plugin.
///
-///
-/// An example of the docfx.json:
-///
-/// {
-/// "build": {
-/// "content": [
-/// {
-/// "files": [ "**/*.{md,yml}" ],
-/// "exclude": [ "_site/**" ]
-/// },
-/// {
-/// "files": [ "*.xsd" ],
-/// "src": "../../some-external-location"
-/// },
-/// {
-/// "files": [ "internal-store-folder/*.xsd" ]
-/// }
-/// ],
-/// "resource": [
-/// {
-/// "files": [ "images/**" ]
-/// }
-/// ],
-/// "output": "_site",
-/// "template": [
-/// "default",
-/// "templates/your-template-with"
-/// ],
-/// "fileMetadata": {
-/// "hx.xmldoc.xslt": { "**.xsd": "./xml-to-md.xslt" },
-/// "hx.xmldoc.store": { "../../some-external-location/*.xsd": "internal-store-folder" }
-/// "hx.xmldoc.toc": {
-/// "**/*-some.xsd": { "action": "InsertAfter", "key": "~/articles/introduction.md" },
-/// "**/*-other.xsd": { "action": "AppendChild", "key": "Namespace.Class.whatever.uid" }
-/// }
-/// }
-/// }
-/// }
-///
-///
public class FileMetadata
{
///
@@ -58,9 +18,9 @@ public class FileMetadata
public const string StoreKey = "hx.xmldoc.store";
///
- /// The name of the xslt key in the fileMetadata of content files.
+ /// The name of the template key in the fileMetadata of content files.
///
- public const string XsltKey = "hx.xmldoc.xslt";
+ public const string TemplateKey = "hx.xmldoc.template";
///
/// The name of the Table Of Contents key in the fileMetadata of content files.
@@ -77,10 +37,10 @@ public class FileMetadata
public string Store { get; set; }
///
- /// The hx.xmldoc.xslt file metadata to specify a path to an XSLT file to transform XML-based content files
- /// into Markdown for further generation of HTML output files styled with the documentation templates.
+ /// The hx.xmldoc.template file metadata to specify a path to a template file to transform XML-based
+ /// content files into Markdown for further generation of HTML output files styled with the documentation templates.
///
- public string Xslt { get; set; }
+ public string Template { get; set; }
///
/// The hx.xmldoc.toc file metadata to specify a configuration for
@@ -106,8 +66,8 @@ public static FileMetadata From(IDictionary dictionary)
dictionary.TryGetValue(FileMetadata.StoreKey, out var obj);
metadata.Store = obj as string;
- dictionary.TryGetValue(FileMetadata.XsltKey, out obj);
- metadata.Xslt = obj as string;
+ dictionary.TryGetValue(FileMetadata.TemplateKey, out obj);
+ metadata.Template = obj as string;
dictionary.TryGetValue(FileMetadata.TocKey, out var tocObj);
metadata.Toc = TocMetadata.From(tocObj as IDictionary);
diff --git a/src/Heleonix.Docfx.Plugins.XmlDoc/HeaderHandler.cs b/src/Heleonix.Docfx.Plugins.XmlDoc/HeaderHandler.cs
new file mode 100644
index 0000000..31794c4
--- /dev/null
+++ b/src/Heleonix.Docfx.Plugins.XmlDoc/HeaderHandler.cs
@@ -0,0 +1,166 @@
+//
+// Copyright (c) Heleonix - Hennadii Lutsyshyn. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the repository root for full license information.
+//
+
+namespace Heleonix.Docfx.Plugins.XmlDoc;
+
+using global::Docfx.Common;
+using global::Docfx.DataContracts.Common;
+using global::Docfx.Plugins;
+using HtmlAgilityPack;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Net;
+
+/// >
+[Export(nameof(IHeaderHandler), typeof(IHeaderHandler))]
+public class HeaderHandler : IHeaderHandler
+{
+ ///
+ public (string h1, string h1Raw, string body) ExtractH1(string html)
+ {
+ var document = new HtmlDocument();
+
+ document.LoadHtml(html);
+
+ // InnerText in HtmlAgilityPack is not decoded, should be a bug
+ var h1Node = document.DocumentNode.SelectSingleNode("//h1");
+ var h1 = WebUtility.HtmlDecode(h1Node?.InnerText);
+ var h1Raw = string.Empty;
+
+ // If the html content is a fragment, which starts with 'h1' heading, like: Heading
Content
+ if (h1Node != null && GetFirstNoneCommentChild(document.DocumentNode) == h1Node)
+ {
+ h1Raw = h1Node.OuterHtml;
+ h1Node.Remove();
+ }
+
+ return (h1, h1Raw, document.DocumentNode.OuterHtml);
+
+ static HtmlNode GetFirstNoneCommentChild(HtmlNode node)
+ {
+ var result = node.FirstChild;
+
+ while (result != null)
+ {
+ if (result.NodeType == HtmlNodeType.Comment || string.IsNullOrWhiteSpace(result.OuterHtml))
+ {
+ result = result.NextSibling;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ return result;
+ }
+ }
+
+ ///
+ public void HandleYamlHeader(ImmutableDictionary yamlHeader, FileModel model)
+ {
+ if (yamlHeader == null)
+ {
+ return;
+ }
+
+ foreach (var item in yamlHeader.OrderBy(i => i.Key, StringComparer.Ordinal))
+ {
+ var content = (IDictionary)model.Content;
+
+ switch (item.Key)
+ {
+ case Constants.PropertyName.Uid:
+ var uid = item.Value as string;
+
+ if (!string.IsNullOrWhiteSpace(uid))
+ {
+ content[item.Key] = item.Value;
+ model.Uids = new[] { new UidDefinition(uid, model.LocalPathFromRoot) }.ToImmutableArray();
+ }
+
+ break;
+ case Constants.PropertyName.DocumentType:
+ content[item.Key] = item.Value;
+ model.DocumentType = item.Value as string;
+
+ break;
+ case Constants.PropertyName.OutputFileName:
+ content[item.Key] = item.Value;
+
+ var outputFileName = item.Value as string;
+
+ if (!string.IsNullOrWhiteSpace(outputFileName))
+ {
+ if (Path.GetFileName(outputFileName) == outputFileName)
+ {
+ model.File = (RelativePath)model.File + (RelativePath)outputFileName;
+ }
+ else
+ {
+ Logger.LogWarning($"Invalid output file name in yaml header: {outputFileName}, skip rename output file.");
+ }
+ }
+
+ break;
+ default:
+ content[item.Key] = item.Value;
+
+ break;
+ }
+ }
+ }
+
+ ///
+ public string GetTitle(FileModel model, ImmutableDictionary yamlHeader, string h1)
+ {
+ // title from YAML header
+ if (yamlHeader != null && TryGetStringValue(yamlHeader, Constants.PropertyName.Title, out var yamlHeaderTitle))
+ {
+ return yamlHeaderTitle;
+ }
+
+ var content = (IDictionary)model.Content;
+
+ // title from metadata/titleOverwriteH1
+ if (TryGetStringValue(content, Constants.PropertyName.TitleOverwriteH1, out var titleOverwriteH1))
+ {
+ return titleOverwriteH1;
+ }
+
+ // title from H1
+ if (!string.IsNullOrEmpty(h1))
+ {
+ return h1;
+ }
+
+ // title from globalMetadata or fileMetadata
+ if (TryGetStringValue(content, Constants.PropertyName.Title, out var title))
+ {
+ return title;
+ }
+
+ return Path.GetFileNameWithoutExtension(model.File);
+ }
+
+ private static bool TryGetStringValue(IDictionary dictionary, string key, out string strValue)
+ {
+ if (dictionary.TryGetValue(key, out var value) && value is string str && !string.IsNullOrEmpty(str))
+ {
+ strValue = str;
+
+ return true;
+ }
+ else
+ {
+ strValue = null;
+
+ return false;
+ }
+ }
+}
diff --git a/src/Heleonix.Docfx.Plugins.XmlDoc/Heleonix.Docfx.Plugins.XmlDoc.csproj b/src/Heleonix.Docfx.Plugins.XmlDoc/Heleonix.Docfx.Plugins.XmlDoc.csproj
index 17faf73..5c64cc5 100644
--- a/src/Heleonix.Docfx.Plugins.XmlDoc/Heleonix.Docfx.Plugins.XmlDoc.csproj
+++ b/src/Heleonix.Docfx.Plugins.XmlDoc/Heleonix.Docfx.Plugins.XmlDoc.csproj
@@ -22,11 +22,12 @@
+
-
+
-
+
@@ -35,6 +36,10 @@
+
+
+
+
diff --git a/src/Heleonix.Docfx.Plugins.XmlDoc/IHeaderHandler.cs b/src/Heleonix.Docfx.Plugins.XmlDoc/IHeaderHandler.cs
new file mode 100644
index 0000000..1d2335d
--- /dev/null
+++ b/src/Heleonix.Docfx.Plugins.XmlDoc/IHeaderHandler.cs
@@ -0,0 +1,39 @@
+//
+// Copyright (c) Heleonix - Hennadii Lutsyshyn. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the repository root for full license information.
+//
+
+namespace Heleonix.Docfx.Plugins.XmlDoc;
+
+using System.Collections.Immutable;
+using global::Docfx.Plugins;
+
+///
+/// Handles headers and titles of the generated html result content.
+///
+public interface IHeaderHandler
+{
+ ///
+ /// Extracts the 'h1' header from the html result content and returns both.
+ ///
+ /// The html result content to extract 'h1' header from.
+ /// Extracted 'h1' header and content.
+ (string h1, string h1Raw, string body) ExtractH1(string html);
+
+ ///
+ /// Handles the yaml header of the markdown to apply defined values in the yaml header.
+ ///
+ /// The yaml header fo the html result contents of the
+ /// to handle.
+ /// The model of the markdown content to handle the yaml header for.
+ void HandleYamlHeader(ImmutableDictionary yamlHeader, FileModel model);
+
+ ///
+ /// Gets a title from the passed sources.
+ ///
+ /// A model to try to get a title from.
+ /// A Yaml Header of the markdown file to get a title from.
+ /// A 'h1' header to return as a title.
+ /// A title from the provided sources.
+ string GetTitle(FileModel model, ImmutableDictionary yamlHeader, string h1);
+}
diff --git a/src/Heleonix.Docfx.Plugins.XmlDoc/ITocHandler.cs b/src/Heleonix.Docfx.Plugins.XmlDoc/ITocHandler.cs
new file mode 100644
index 0000000..4538562
--- /dev/null
+++ b/src/Heleonix.Docfx.Plugins.XmlDoc/ITocHandler.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) Heleonix - Hennadii Lutsyshyn. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the repository root for full license information.
+//
+
+namespace Heleonix.Docfx.Plugins.XmlDoc;
+
+using System.Collections.Generic;
+using global::Docfx.Plugins;
+
+///
+/// Handles Table of Contents actions.
+///
+public interface ITocHandler
+{
+ ///
+ /// Builds TOC restructures and adds into the .
+ ///
+ /// Content model of a file to handle.
+ /// Common list of TOC restructures to add handled TOC for .
+ void HandleTocRestructions(FileModel model, IList restructions);
+}
diff --git a/src/Heleonix.Docfx.Plugins.XmlDoc/ITransformer.cs b/src/Heleonix.Docfx.Plugins.XmlDoc/ITransformer.cs
new file mode 100644
index 0000000..803775f
--- /dev/null
+++ b/src/Heleonix.Docfx.Plugins.XmlDoc/ITransformer.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) Heleonix - Hennadii Lutsyshyn. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the repository root for full license information.
+//
+
+namespace Heleonix.Docfx.Plugins.XmlDoc;
+
+using global::Docfx.Plugins;
+
+///
+/// Declares functionality to transform xml-based files into markdown using different templates.
+///
+public interface ITransformer
+{
+ ///
+ /// Transforms the specified model with the template specified in metadata.
+ ///
+ /// The xml model to transform.
+ /// A host service from Docfx.
+ /// The string containing the transformed contents.
+ string Transform(FileModel model, IHostService host);
+}
\ No newline at end of file
diff --git a/src/Heleonix.Docfx.Plugins.XmlDoc/TocHandler.cs b/src/Heleonix.Docfx.Plugins.XmlDoc/TocHandler.cs
new file mode 100644
index 0000000..91b2a39
--- /dev/null
+++ b/src/Heleonix.Docfx.Plugins.XmlDoc/TocHandler.cs
@@ -0,0 +1,59 @@
+//
+// Copyright (c) Heleonix - Hennadii Lutsyshyn. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the repository root for full license information.
+//
+
+namespace Heleonix.Docfx.Plugins.XmlDoc;
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Composition;
+using global::Docfx.DataContracts.Common;
+using global::Docfx.Plugins;
+
+///
+[Export(nameof(ITocHandler), typeof(ITocHandler))]
+public class TocHandler : ITocHandler
+{
+ ///
+ public void HandleTocRestructions(FileModel model, IList restructions)
+ {
+ var content = (IDictionary)model.Content;
+
+ var metadata = FileMetadata.From(content);
+
+ if (metadata.Toc.Key == null)
+ {
+ return;
+ }
+
+ var treeItem = new TreeItem();
+
+ treeItem.Metadata[Constants.PropertyName.Name] = content[Constants.PropertyName.Title];
+ treeItem.Metadata[Constants.PropertyName.Href] = model.Key;
+ treeItem.Metadata[Constants.PropertyName.TopicHref] = model.Key;
+
+ if (content.ContainsKey("_appName"))
+ {
+ treeItem.Metadata["_appName"] = content["_appName"];
+ }
+
+ if (content.ContainsKey("_appTitle"))
+ {
+ treeItem.Metadata["_appTitle"] = content["_appTitle"];
+ }
+
+ if (content.ContainsKey("_enableSearch"))
+ {
+ treeItem.Metadata["_enableSearch"] = content["_enableSearch"];
+ }
+
+ restructions.Add(new ()
+ {
+ ActionType = metadata.Toc.Action,
+ TypeOfKey = metadata.Toc.Key.StartsWith('~') ? TreeItemKeyType.TopicHref : TreeItemKeyType.TopicUid,
+ Key = metadata.Toc.Key,
+ RestructuredItems = new List { treeItem }.ToImmutableList(),
+ });
+ }
+}
diff --git a/src/Heleonix.Docfx.Plugins.XmlDoc/Transformer.cs b/src/Heleonix.Docfx.Plugins.XmlDoc/Transformer.cs
new file mode 100644
index 0000000..c56333a
--- /dev/null
+++ b/src/Heleonix.Docfx.Plugins.XmlDoc/Transformer.cs
@@ -0,0 +1,129 @@
+//
+// Copyright (c) Heleonix - Hennadii Lutsyshyn. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the repository root for full license information.
+//
+
+namespace Heleonix.Docfx.Plugins.XmlDoc;
+
+using System.Collections.Concurrent;
+using System.Composition;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Xml.Linq;
+using System.Xml.Xsl;
+using global::Docfx.Plugins;
+using RazorEngineCore;
+
+/// >
+[Export(nameof(ITransformer), typeof(ITransformer))]
+public class Transformer : ITransformer
+{
+ private static readonly RazorEngine Engine = new ();
+
+ private readonly ConcurrentDictionary xmlTemplates = new ();
+
+ private readonly ConcurrentDictionary>>
+ razorTemplates = new ();
+
+ static Transformer()
+ {
+ AssemblyLoadContext.Default.Resolving += Transformer.Default_Resolving;
+ }
+
+ ///
+ public string Transform(FileModel model, IHostService host)
+ {
+ try
+ {
+ var content = (IDictionary)model.Content;
+
+ var metadata = FileMetadata.From(content);
+
+ if (".xslt".Equals(Path.GetExtension(metadata.Template), StringComparison.OrdinalIgnoreCase))
+ {
+ return this.TransformWithXslt(model, metadata);
+ }
+ else if (".cshtml".Equals(Path.GetExtension(metadata.Template), StringComparison.OrdinalIgnoreCase))
+ {
+ return this.TransformWithRazor(model, metadata);
+ }
+
+ host.LogError($"Unknown template file format: {metadata.Template}.", model.FileAndType.FullPath);
+
+ return null;
+ }
+ catch (Exception ex)
+ {
+ host.LogError(ex.ToString(), model.FileAndType.FullPath);
+
+ throw;
+ }
+ }
+
+ ///
+ /// Resolves dependencies manually, because MEF2 in Docfx does not handle plugin's dependencies.
+ /// In unit tests dependencies are resolved automatically, so this method does not need to be covered by tests.
+ ///
+ /// The assembly load context. Not used.
+ /// The name of the dependency assembly to load manually.
+ /// Returns the resolved assembly, which is dependency for this plugin, otherwise null.
+ [ExcludeFromCodeCoverage]
+ private static Assembly Default_Resolving(AssemblyLoadContext context, AssemblyName assemblyName)
+ {
+#pragma warning disable S3885 // "Assembly.Load" should be used
+ if (assemblyName.Name.Equals("RazorEngineCore", StringComparison.OrdinalIgnoreCase))
+ {
+ var path = Path.Combine(
+ Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
+ "RazorEngineCore.dll");
+
+ return Assembly.LoadFile(path);
+ }
+
+ if (assemblyName.Name.Equals("HtmlAgilityPack", StringComparison.OrdinalIgnoreCase))
+ {
+ var path = Path.Combine(
+ Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
+ "HtmlAgilityPack.dll");
+
+ return Assembly.LoadFile(path);
+#pragma warning restore S3885 // "Assembly.Load" should be used
+ }
+
+ return null;
+ }
+
+ private string TransformWithXslt(FileModel model, FileMetadata metadata)
+ {
+ var template = this.xmlTemplates.GetOrAdd(
+ metadata.Template,
+ (k, arg) =>
+ {
+ var t = new XslCompiledTransform();
+ t.Load(arg);
+ return t;
+ }, metadata.Template);
+
+ using var stringWriter = new StringWriter();
+
+ var args = new XsltArgumentList();
+
+ args.AddParam("filename", string.Empty, Path.GetFileNameWithoutExtension(model.File));
+
+ template.Transform(model.FileAndType.FullPath, args, stringWriter);
+
+ return stringWriter.ToString();
+ }
+
+ private string TransformWithRazor(FileModel model, FileMetadata metadata)
+ {
+ var template = this.razorTemplates.GetOrAdd(
+ metadata.Template,
+ (k, arg) => Engine.Compile>(File.ReadAllText(arg)),
+ metadata.Template);
+
+ return template.Run(instance =>
+ instance.Model = XDocument.Load(model.FileAndType.FullPath, LoadOptions.SetBaseUri));
+ }
+}
diff --git a/src/Heleonix.Docfx.Plugins.XmlDoc/XmlDocBuildStep.cs b/src/Heleonix.Docfx.Plugins.XmlDoc/XmlDocBuildStep.cs
index e9e8b8b..f7c1fce 100644
--- a/src/Heleonix.Docfx.Plugins.XmlDoc/XmlDocBuildStep.cs
+++ b/src/Heleonix.Docfx.Plugins.XmlDoc/XmlDocBuildStep.cs
@@ -5,21 +5,12 @@
namespace Heleonix.Docfx.Plugins.XmlDoc;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Reflection;
-using System.Runtime.Loader;
-using System.Xml.Xsl;
using global::Docfx.Common;
using global::Docfx.DataContracts.Common;
using global::Docfx.Plugins;
-using HtmlAgilityPack;
///
/// The build step to generate html documentation from the xml-based content files.
@@ -28,15 +19,23 @@ namespace Heleonix.Docfx.Plugins.XmlDoc;
[Export(nameof(XmlDocProcessor), typeof(IDocumentBuildStep))]
public class XmlDocBuildStep : IDocumentBuildStep
{
- private readonly ConcurrentDictionary transforms = new ();
+ ///
+ /// Gets or sets the transformer to use to transform xml-based contents into markdown.
+ ///
+ [Import(nameof(ITransformer))]
+ public ITransformer Transformer { get; set; }
///
- /// Initializes a new instance of the class.
+ /// Gets or sets the header handler to handle headers and titles of the generated html result content.
///
- public XmlDocBuildStep()
- {
- AssemblyLoadContext.Default.Resolving += XmlDocBuildStep.Default_Resolving;
- }
+ [Import(nameof(IHeaderHandler))]
+ public IHeaderHandler HeaderHandler { get; set; }
+
+ ///
+ /// Gets or sets the handler of Table of Contents actions specified for xml-based files.
+ ///
+ [Import(nameof(ITocHandler))]
+ public ITocHandler TocHandler { get; set; }
///
/// Gets the order of the build step to be executed with.
@@ -71,7 +70,7 @@ public void Postbuild(ImmutableList models, IHostService host)
}
///
- /// Builds the xml-based contents via Markdown XSLT transformation into HTML output.
+ /// Builds the xml-based contents via transformations into Markdown.
///
/// The models to transform from XML into HTML output.
/// The host to be used for common tasks.
@@ -91,60 +90,30 @@ public IEnumerable Prebuild(ImmutableList models, IHostSer
var content = (Dictionary)model.Content;
- var metadata = FileMetadata.From(content);
-
- var transform = this.transforms.GetOrAdd(
- metadata.Xslt,
- (k, arg) =>
- {
- var t = new XslCompiledTransform();
- t.Load(arg);
- return t;
- }, metadata.Xslt);
-
- using (var stringWriter = new StringWriter())
- {
- var args = new XsltArgumentList();
-
- args.AddParam("filename", string.Empty, Path.GetFileNameWithoutExtension(model.File));
-
- transform.Transform(model.FileAndType.FullPath, args, stringWriter);
-
- content[Constants.PropertyName.Conceptual] = stringWriter.ToString();
- }
-
- var markdown = (string)content[Constants.PropertyName.Conceptual];
+ var markdown = this.Transformer.Transform(model, host);
var result = host.Markup(markdown, model.FileAndType, false);
- var (h1, h1Raw, conceptual) = XmlDocBuildStep.ExtractH1(result.Html);
+ var (h1, h1Raw, conceptual) = this.HeaderHandler.ExtractH1(result.Html);
content["rawTitle"] = h1Raw;
if (!string.IsNullOrEmpty(h1Raw))
{
- model.ManifestProperties.rawTitle = h1Raw;
+ (model.ManifestProperties as IDictionary)["rawTitle"] = h1Raw;
}
content[Constants.PropertyName.Conceptual] = conceptual;
- if (result.YamlHeader != null)
- {
- foreach (var item in result.YamlHeader.OrderBy(i => i.Key, StringComparer.Ordinal))
- {
- XmlDocBuildStep.HandleYamlHeaderPair(model, item.Key, item.Value);
- }
- }
+ this.HeaderHandler.HandleYamlHeader(result.YamlHeader, model);
- content[Constants.PropertyName.Title] =
- XmlDocBuildStep.GetTitle(content, result.YamlHeader, h1)
- ?? Path.GetFileNameWithoutExtension(model.File);
+ content[Constants.PropertyName.Title] = this.HeaderHandler.GetTitle(model, result.YamlHeader, h1);
model.LinkToFiles = result.LinkToFiles.ToImmutableHashSet();
model.LinkToUids = result.LinkToUids;
model.FileLinkSources = result.FileLinkSources;
model.UidLinkSources = result.UidLinkSources;
- model.Properties.XrefSpec = null;
+ (model.Properties as IDictionary)["XrefSpec"] = null;
if (model.Uids.Length > 0)
{
@@ -158,192 +127,11 @@ public IEnumerable Prebuild(ImmutableList models, IHostSer
(model.Properties as IDictionary)["XrefSpec"] = xrefSpec;
}
- if (metadata.Toc.Key == null)
- {
- continue;
- }
-
- var treeItem = new TreeItem();
-
- treeItem.Metadata[Constants.PropertyName.Name] = content[Constants.PropertyName.Title];
- treeItem.Metadata[Constants.PropertyName.Href] = model.Key;
- treeItem.Metadata[Constants.PropertyName.TopicHref] = model.Key;
-
- if (content.ContainsKey("_appName"))
- {
- treeItem.Metadata["_appName"] = content["_appName"];
- }
-
- if (content.ContainsKey("_appTitle"))
- {
- treeItem.Metadata["_appTitle"] = content["_appTitle"];
- }
-
- if (content.ContainsKey("_enableSearch"))
- {
- treeItem.Metadata["_enableSearch"] = content["_enableSearch"];
- }
-
- tocRestructions.Add(new ()
- {
- ActionType = metadata.Toc.Action,
- TypeOfKey = metadata.Toc.Key.StartsWith("~") ? TreeItemKeyType.TopicHref : TreeItemKeyType.TopicUid,
- Key = metadata.Toc.Key,
- RestructuredItems = new List { treeItem }.ToImmutableList(),
- });
+ this.TocHandler.HandleTocRestructions(model, tocRestructions);
}
host.TableOfContentRestructions = tocRestructions.ToImmutableList();
return models;
}
-
- [ExcludeFromCodeCoverage]
- private static Assembly Default_Resolving(AssemblyLoadContext context, AssemblyName assemblyName)
- {
- if (assemblyName.Name.Equals("HtmlAgilityPack", StringComparison.OrdinalIgnoreCase))
- {
- var path = Path.Combine(
- Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
- "HtmlAgilityPack.dll");
-
-#pragma warning disable S3885 // "Assembly.Load" should be used
- return Assembly.LoadFile(path);
-#pragma warning restore S3885 // "Assembly.Load" should be used
- }
-
- return null;
- }
-
- private static (string h1, string h1Raw, string body) ExtractH1(string contentHtml)
- {
- var document = new HtmlDocument();
-
- document.LoadHtml(contentHtml);
-
- // InnerText in HtmlAgilityPack is not decoded, should be a bug
- var h1Node = document.DocumentNode.SelectSingleNode("//h1");
- var h1 = WebUtility.HtmlDecode(h1Node?.InnerText);
- var h1Raw = string.Empty;
-
- // If the html content is a fragment, which starts with 'h1' heading, like: Heading
Content
- if (h1Node != null && GetFirstNoneCommentChild(document.DocumentNode) == h1Node)
- {
- h1Raw = h1Node.OuterHtml;
- h1Node.Remove();
- }
-
- return (h1, h1Raw, document.DocumentNode.OuterHtml);
-
- static HtmlNode GetFirstNoneCommentChild(HtmlNode node)
- {
- var result = node.FirstChild;
-
- while (result != null)
- {
- if (result.NodeType == HtmlNodeType.Comment || string.IsNullOrWhiteSpace(result.OuterHtml))
- {
- result = result.NextSibling;
- }
- else
- {
- break;
- }
- }
-
- return result;
- }
- }
-
- private static void HandleYamlHeaderPair(FileModel model, string key, object value)
- {
- var content = (IDictionary)model.Content;
-
- switch (key)
- {
- case Constants.PropertyName.Uid:
- var uid = value as string;
-
- if (!string.IsNullOrWhiteSpace(uid))
- {
- content[key] = value;
- model.Uids = new[] { new UidDefinition(uid, model.LocalPathFromRoot) }.ToImmutableArray();
- }
-
- break;
- case Constants.PropertyName.DocumentType:
- content[key] = value;
- model.DocumentType = value as string;
-
- break;
- case Constants.PropertyName.OutputFileName:
- content[key] = value;
-
- var outputFileName = value as string;
-
- if (!string.IsNullOrWhiteSpace(outputFileName))
- {
- if (Path.GetFileName(outputFileName) == outputFileName)
- {
- model.File = (RelativePath)model.File + (RelativePath)outputFileName;
- }
- else
- {
- Logger.LogWarning($"Invalid output file name in yaml header: {outputFileName}, skip rename output file.");
- }
- }
-
- break;
- default:
- content[key] = value;
-
- break;
- }
- }
-
- private static string GetTitle(IDictionary content, ImmutableDictionary yamlHeader, string h1)
- {
- // title from YAML header
- if (yamlHeader != null
- && TryGetStringValue(yamlHeader, Constants.PropertyName.Title, out var yamlHeaderTitle))
- {
- return yamlHeaderTitle;
- }
-
- // title from metadata/titleOverwriteH1
- if (TryGetStringValue(content, Constants.PropertyName.TitleOverwriteH1, out var titleOverwriteH1))
- {
- return titleOverwriteH1;
- }
-
- // title from H1
- if (!string.IsNullOrEmpty(h1))
- {
- return h1;
- }
-
- // title from globalMetadata or fileMetadata
- if (TryGetStringValue(content, Constants.PropertyName.Title, out var title))
- {
- return title;
- }
-
- return null;
- }
-
- private static bool TryGetStringValue(IDictionary dictionary, string key, out string strValue)
- {
- if (dictionary.TryGetValue(key, out var value) && value is string str && !string.IsNullOrEmpty(str))
- {
- strValue = str;
-
- return true;
- }
- else
- {
- strValue = null;
-
- return false;
- }
- }
}
diff --git a/src/Heleonix.Docfx.Plugins.XmlDoc/XmlDocProcessor.cs b/src/Heleonix.Docfx.Plugins.XmlDoc/XmlDocProcessor.cs
index de2a308..74c552d 100644
--- a/src/Heleonix.Docfx.Plugins.XmlDoc/XmlDocProcessor.cs
+++ b/src/Heleonix.Docfx.Plugins.XmlDoc/XmlDocProcessor.cs
@@ -74,7 +74,8 @@ public XmlDocProcessor()
/// otherwise .
public ProcessingPriority GetProcessingPriority(FileAndType file)
{
- if (file.Type == DocumentType.Article && this.Settings.SupportedFormats.Contains(Path.GetExtension(file.File)))
+ if (file.Type == DocumentType.Article
+ && this.Settings.SupportedFormats.Contains(Path.GetExtension(file.File), StringComparer.OrdinalIgnoreCase))
{
this.ContentFiles.TryAdd(file.File, string.Empty);
@@ -136,9 +137,9 @@ public FileModel Load(FileAndType file, ImmutableDictionary meta
content[key] = value;
}
- if (PathUtility.IsRelativePath(fileMetadata.Xslt))
+ if (PathUtility.IsRelativePath(fileMetadata.Template))
{
- content[FileMetadata.XsltKey] = Path.Combine(EnvironmentContext.BaseDirectory, fileMetadata.Xslt);
+ content[FileMetadata.TemplateKey] = Path.Combine(EnvironmentContext.BaseDirectory, fileMetadata.Template);
}
content[Constants.PropertyName.SystemKeys] = this.systemKeys;
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/FileMetadataTests.cs b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/FileMetadataTests.cs
index b05ee8d..3463284 100644
--- a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/FileMetadataTests.cs
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/FileMetadataTests.cs
@@ -36,7 +36,7 @@ public static void From()
Should("return the empty FileMetadata instance", () =>
{
Assert.That(metadata.Store, Is.Null);
- Assert.That(metadata.Xslt, Is.Null);
+ Assert.That(metadata.Template, Is.Null);
Assert.That(metadata.Toc, Is.Null);
});
});
@@ -46,7 +46,7 @@ public static void From()
dictionary = new Dictionary
{
{ FileMetadata.StoreKey, "./store" },
- { FileMetadata.XsltKey, "./transform.xslt" },
+ { FileMetadata.TemplateKey, "./transform.xslt" },
{
FileMetadata.TocKey, new Dictionary
{
@@ -59,7 +59,7 @@ public static void From()
Should("return the filled in FileMetadata instance", () =>
{
Assert.That(metadata.Store, Is.EqualTo("./store"));
- Assert.That(metadata.Xslt, Is.EqualTo("./transform.xslt"));
+ Assert.That(metadata.Template, Is.EqualTo("./transform.xslt"));
Assert.That(metadata.Toc.Key, Is.EqualTo("SomeToc"));
Assert.That(metadata.Toc.Action, Is.EqualTo(TreeItemActionType.InsertAfter));
});
@@ -70,13 +70,13 @@ public static void From()
dictionary = new Dictionary
{
{ FileMetadata.StoreKey, "./store" },
- { FileMetadata.XsltKey, "./transform.xslt" },
+ { FileMetadata.TemplateKey, "./transform.xslt" },
};
Should("return the filled in FileMetadata instance without TOC", () =>
{
Assert.That(metadata.Store, Is.EqualTo("./store"));
- Assert.That(metadata.Xslt, Is.EqualTo("./transform.xslt"));
+ Assert.That(metadata.Template, Is.EqualTo("./transform.xslt"));
Assert.That(metadata.Toc.Key, Is.Null);
Assert.That(metadata.Toc.Action, Is.EqualTo(TreeItemActionType.ReplaceSelf));
});
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/HeaderHandlerTests.cs b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/HeaderHandlerTests.cs
new file mode 100644
index 0000000..147a518
--- /dev/null
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/HeaderHandlerTests.cs
@@ -0,0 +1,273 @@
+//
+// Copyright (c) Heleonix - Hennadii Lutsyshyn. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the repository root for full license information.
+//
+
+namespace Heleonix.Docfx.Plugins.XmlDoc.Tests;
+
+using global::Docfx.DataContracts.Common;
+using System.Collections.Immutable;
+
+///
+/// Tests the .
+///
+[ComponentTest(Type = typeof(HeaderHandler))]
+public class HeaderHandlerTests
+{
+ ///
+ /// Tests the .
+ ///
+ [MemberTest(Name = nameof(HeaderHandler.ExtractH1))]
+ public void ExtractH1()
+ {
+ var headerHandler = new HeaderHandler();
+ string html = null;
+ var result = default((string h1, string h1Raw, string body));
+
+ When("the method is called", () =>
+ {
+ Act(() =>
+ {
+ result = headerHandler.ExtractH1(html);
+ });
+
+ And("there is a full html content", () =>
+ {
+ Arrange(() =>
+ {
+ html = "Heading
Content
";
+ });
+
+ Should("extract 'h1' and body with 'h1'", () =>
+ {
+ Assert.That(result.h1, Is.EqualTo("Heading"));
+ Assert.That(result.h1Raw, Is.Empty);
+ Assert.That(result.body, Is.EqualTo(html));
+ });
+ });
+
+ And("the loaded html content is a fragment of html document", () =>
+ {
+ Arrange(() =>
+ {
+ html = " Heading
Text
";
+ });
+
+ Should("extract 'h1' and body without 'h1'", () =>
+ {
+ Assert.That(result.h1, Is.EqualTo("Heading"));
+ Assert.That(result.h1Raw, Is.EqualTo("Heading
"));
+ Assert.That(result.body, Is.EqualTo(" Text
"));
+ });
+ });
+
+ And("there is no 'h1' html tag", () =>
+ {
+ Arrange(() =>
+ {
+ html = "Text
";
+ });
+
+ Should("return body", () =>
+ {
+ Assert.That(result.h1, Is.Null);
+ Assert.That(result.h1Raw, Is.Empty);
+ Assert.That(result.body, Is.EqualTo("Text
"));
+ });
+ });
+ });
+ }
+
+ ///
+ /// Tests the .
+ ///
+ [MemberTest(Name = nameof(HeaderHandler.HandleYamlHeader))]
+ public void HandleYamlHeader()
+ {
+ var headerHandler = new HeaderHandler();
+ ImmutableDictionary yamlHeader = null;
+ FileModel model = null;
+
+ When("the method is called", () =>
+ {
+ Arrange(() =>
+ {
+ model = new FileModel(
+ new FileAndType("X:/Base", "some\\file.xsd", DocumentType.Article),
+ new Dictionary());
+ });
+
+ Act(() =>
+ {
+ headerHandler.HandleYamlHeader(yamlHeader, model);
+ });
+
+ And("the yaml header is not provided", () =>
+ {
+ Arrange(() =>
+ {
+ yamlHeader = null;
+ model = null;
+ });
+
+ Should("do nothing", () =>
+ {
+ Assert.That(model, Is.Null);
+ });
+ });
+
+ And("the yaml header is provided", () =>
+ {
+ yamlHeader = new Dictionary
+ {
+ { Constants.PropertyName.Uid, "some-uid" },
+ { Constants.PropertyName.DocumentType, "Conceptual" },
+ { Constants.PropertyName.OutputFileName, "output-file-name.html" },
+ { Constants.PropertyName.Title, "Some Title" },
+ { "any_other_key", "any-other-value" },
+ }.ToImmutableDictionary();
+
+ Should("populate the model with properties specified in the yaml header", () =>
+ {
+ var content = model.Content as Dictionary;
+
+ Assert.That(content[Constants.PropertyName.Uid], Contains.Substring("some-uid"));
+ Assert.That(model.Uids[0].Name, Is.EqualTo("some-uid"));
+ Assert.That(model.Uids[0].File, Is.EqualTo(model.LocalPathFromRoot));
+
+ Assert.That(content[Constants.PropertyName.DocumentType], Is.EqualTo("Conceptual"));
+ Assert.That(model.DocumentType, Is.EqualTo("Conceptual"));
+
+ Assert.That(content[Constants.PropertyName.OutputFileName], Is.EqualTo("output-file-name.html"));
+ Assert.That(model.File, Is.EqualTo("some/output-file-name.html"));
+
+ Assert.That(content["any_other_key"], Is.EqualTo("any-other-value"));
+ });
+
+ And("the yaml header has an incorrect output file name", () =>
+ {
+ yamlHeader = new Dictionary
+ {
+ { Constants.PropertyName.OutputFileName, "extra-path/output-file-name.html" },
+ }.ToImmutableDictionary();
+
+ Should("keep the File in the model unchanged", () =>
+ {
+ Assert.That(model.File, Is.EqualTo("some/file.xsd"));
+ });
+ });
+ });
+ });
+ }
+
+ ///
+ /// Tests the .
+ ///
+ [MemberTest(Name = nameof(HeaderHandler.GetTitle))]
+ public void GetTitle()
+ {
+ var headerHandler = new HeaderHandler();
+ FileModel model = null;
+ ImmutableDictionary yamlHeader = null;
+ string h1 = null;
+ var result = default(string);
+
+ When("the method is called", () =>
+ {
+ Act(() =>
+ {
+ result = headerHandler.GetTitle(model, yamlHeader, h1);
+ });
+
+ And("there is a yaml header specified with a title", () =>
+ {
+ Arrange(() =>
+ {
+ yamlHeader = new Dictionary
+ {
+ { Constants.PropertyName.Title, "Title" },
+ }.ToImmutableDictionary();
+ });
+
+ Should("return a title from the yaml header", () =>
+ {
+ Assert.That(result, Is.EqualTo("Title"));
+ });
+ });
+
+ And("the title is specified in metadata/titleOverwriteH1", () =>
+ {
+ Arrange(() =>
+ {
+ yamlHeader = null;
+
+ model = new FileModel(
+ new FileAndType("X:/Base", "file.xsd", DocumentType.Article),
+ new Dictionary
+ {
+ { Constants.PropertyName.TitleOverwriteH1, "Title 1" },
+ });
+ });
+
+ Should("return a title from the metadata/titleOverwriteH1", () =>
+ {
+ Assert.That(result, Is.EqualTo("Title 1"));
+ });
+ });
+
+ And("the title is specified in global metadata/title", () =>
+ {
+ Arrange(() =>
+ {
+ yamlHeader = null;
+
+ model = new FileModel(
+ new FileAndType("X:/Base", "file.xsd", DocumentType.Article),
+ new Dictionary
+ {
+ { Constants.PropertyName.Title, "Title 2" },
+ });
+ });
+
+ Should("return a title from the metadata/title", () =>
+ {
+ Assert.That(result, Is.EqualTo("Title 2"));
+ });
+ });
+
+ And("the title is specified in the 'h1' parameter", () =>
+ {
+ Arrange(() =>
+ {
+ yamlHeader = null;
+ model = new FileModel(
+ new FileAndType("X:/Base", "file.xsd", DocumentType.Article),
+ new Dictionary());
+ h1 = "Title 3";
+ });
+
+ Should("return a title from the metadata/title", () =>
+ {
+ Assert.That(result, Is.EqualTo("Title 3"));
+ });
+ });
+
+ And("no title is specified", () =>
+ {
+ Arrange(() =>
+ {
+ yamlHeader = null;
+ model = new FileModel(
+ new FileAndType("X:/Base", "file.xsd", DocumentType.Article),
+ new Dictionary());
+ h1 = null;
+ });
+
+ Should("return a title as the file name", () =>
+ {
+ Assert.That(result, Is.EqualTo("file"));
+ });
+ });
+ });
+ }
+}
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Heleonix.Docfx.Plugins.XmlDoc.Tests.csproj b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Heleonix.Docfx.Plugins.XmlDoc.Tests.csproj
index da709c5..f51d8d7 100644
--- a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Heleonix.Docfx.Plugins.XmlDoc.Tests.csproj
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Heleonix.Docfx.Plugins.XmlDoc.Tests.csproj
@@ -2,7 +2,6 @@
net7.0
false
- en
@@ -15,10 +14,13 @@
Always
+
+ Always
+
-
+
@@ -26,10 +28,10 @@
-
+
-
+
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Input.xsd b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Input.xsd
index a916b8c..d82b261 100644
--- a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Input.xsd
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Input.xsd
@@ -4,9 +4,6 @@
elementFormDefault="qualified"
targetNamespace="http://schemas.microsoft.com/developer/msbuild/2003"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
-
-
A path to the NetBuild artifacts directory.
@@ -22,49 +19,4 @@
The file with public/private keys pair to sign assemblies.
-
-
-
- The semantic version. It is passed as /p:Version property to the Build target.
- Default is a version retrieved from $Hx_ChangeLog_ArtifactsDir/semver.txt.
-
-
-
-
-
-
- The .NET Assembly version, like '1.0.0.0'. It is passed as /p:AssemblyVersion property to the Build target.
- Default version is composed as $Hx_NetBuild_Version.$Hx_Run_Number.
-
-
-
-
-
-
- A text file with package release notes. It is passed as /p:PackageReleaseNotes property into the Build target.
- Default is $Hx_ChangeLog_ArtifactsDir/ReleaseNotes.txt.
-
-
-
-
-
-
- Files to delete during cleaning.
-
-
-
-
- Directories to delete during cleaning.
-
-
-
-
- Directories to clean but not delete during cleaning.
-
-
-
-
- Custom files to be copied to the artifacts directory.
-
-
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Properties/Usings.cs b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Properties/Usings.cs
index 2230036..18c08fb 100644
--- a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Properties/Usings.cs
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Properties/Usings.cs
@@ -4,6 +4,7 @@
//
#pragma warning disable SA1200 // Using directives should be placed correctly
+global using global::Docfx.Plugins;
global using Heleonix.Docfx.Plugins.XmlDoc;
global using Heleonix.Testing.NUnit.Aaa;
global using NUnit.Framework;
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Template.cshtml b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Template.cshtml
new file mode 100644
index 0000000..03d6d58
--- /dev/null
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Template.cshtml
@@ -0,0 +1,35 @@
+@using System
+@using System.IO
+@using System.Xml.Linq
+@inherits RazorEngineCore.RazorEngineTemplateBase
+@{
+ XDocument model = Model;
+ XNamespace xs = "http://www.w3.org/2001/XMLSchema";
+
+ var fileName = Path.GetFileNameWithoutExtension(model.Document.BaseUri);
+ var elements = model.Document.Element(xs + "schema").Elements(xs + "element");
+ var desc = elements.FirstOrDefault(e => e.Attribute("name")?.Value == fileName)
+ ?.Element(xs + "annotation")
+ ?.Element(xs + "documentation")?.Value?.Trim();
+ var props = elements.Where(e => e.Attribute("substitutionGroup")?.Value == "msb:Property");
+}
+---
+uid: @fileName
+---
+
+# @fileName
+
+@desc
+
+@if (props.Count() > 0)
+{
+ ## Properties
+ @:
+ foreach (var prop in props)
+ {
+ #### @prop.Attribute("name").Value
+ @:
+ @prop.Element(xs + "annotation").Element(xs + "documentation").Value.Trim()
+ @:
+ }
+}
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Template.xslt b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Template.xslt
index 3544e09..5b94b17 100644
--- a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Template.xslt
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/Template.xslt
@@ -15,12 +15,5 @@
-### Items
-
-####
-
-
-
-
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/TocHandlerTests.cs b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/TocHandlerTests.cs
new file mode 100644
index 0000000..4c5dcc0
--- /dev/null
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/TocHandlerTests.cs
@@ -0,0 +1,137 @@
+//
+// Copyright (c) Heleonix - Hennadii Lutsyshyn. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the repository root for full license information.
+//
+
+namespace Heleonix.Docfx.Plugins.XmlDoc.Tests;
+
+using global::Docfx.DataContracts.Common;
+using Moq;
+
+///
+/// Tests the .
+///
+[ComponentTest(Type = typeof(TocHandler))]
+public class TocHandlerTests
+{
+ ///
+ /// Tests the .
+ ///
+ [MemberTest(Name = nameof(TocHandler.HandleTocRestructions))]
+ public void HandleTocRestructions()
+ {
+ var tocHandler = new TocHandler();
+ FileModel model = null;
+ IList restructions = null;
+
+ When("the method is called", () =>
+ {
+ Arrange(() =>
+ {
+ restructions = new List();
+ });
+
+ Act(() =>
+ {
+ tocHandler.HandleTocRestructions(model, restructions);
+ });
+
+ And("metadata TOC key is null", () =>
+ {
+ Arrange(() =>
+ {
+ model = new FileModel(
+ new FileAndType("X:/BaseDir", "file.xsd", DocumentType.Article),
+ new Dictionary
+ {
+ {
+ FileMetadata.TocKey,
+ new Dictionary
+ {
+ { "action", "AppendChild" },
+ { "key", null },
+ }
+ },
+ });
+ });
+
+ Should("do nothing", () =>
+ {
+ Assert.That(restructions, Is.Empty);
+ });
+ });
+
+ And("metadata TOC Key is Href", () =>
+ {
+ Arrange(() =>
+ {
+ model = new FileModel(
+ new FileAndType("X:/BaseDir", "file.xsd", DocumentType.Article),
+ new Dictionary
+ {
+ { Constants.PropertyName.Title, "Title" },
+ { "_appName", "App name" },
+ { "_appTitle", "App title" },
+ { "_enableSearch", "Enable search" },
+ {
+ FileMetadata.TocKey,
+ new Dictionary
+ {
+ { "action", "AppendChild" },
+ { "key", "~/some/path" },
+ }
+ },
+ });
+ });
+
+ Should("generate one TOC restructure with the specified Href key", () =>
+ {
+ var treeItem = restructions.Single().RestructuredItems.Single();
+
+ Assert.That(treeItem.Metadata[Constants.PropertyName.Name], Is.EqualTo("Title"));
+ Assert.That(treeItem.Metadata[Constants.PropertyName.Href], Is.EqualTo("~/file.xsd"));
+ Assert.That(treeItem.Metadata[Constants.PropertyName.TopicHref], Is.EqualTo("~/file.xsd"));
+ Assert.That(restructions.Single().Key, Is.EqualTo("~/some/path"));
+ Assert.That(restructions.Single().TypeOfKey, Is.EqualTo(TreeItemKeyType.TopicHref));
+ Assert.That(restructions.Single().ActionType, Is.EqualTo(TreeItemActionType.AppendChild));
+ });
+ });
+
+ And("metadata TOC Key is Uid", () =>
+ {
+ Arrange(() =>
+ {
+ model = new FileModel(
+ new FileAndType("X:/BaseDir", "file.xsd", DocumentType.Article),
+ new Dictionary
+ {
+ { Constants.PropertyName.Title, "Title" },
+ { "_appName", "App name" },
+ { "_appTitle", "App title" },
+ { "_enableSearch", "Enable search" },
+ {
+ FileMetadata.TocKey,
+ new Dictionary
+ {
+ { "action", "AppendChild" },
+ { "key", "some.key" },
+ }
+ },
+ });
+ });
+
+ Should("generate one TOC restructure with the specified Uid key", () =>
+ {
+ var treeItem = restructions.Single().RestructuredItems.Single();
+
+ Assert.That(treeItem.Metadata[Constants.PropertyName.Name], Is.EqualTo("Title"));
+ Assert.That(treeItem.Metadata[Constants.PropertyName.Href], Is.EqualTo("~/file.xsd"));
+ Assert.That(treeItem.Metadata[Constants.PropertyName.TopicHref], Is.EqualTo("~/file.xsd"));
+ Assert.That(restructions.Single().Key, Is.EqualTo("some.key"));
+ Assert.That(restructions.Single().TypeOfKey, Is.EqualTo(TreeItemKeyType.TopicUid));
+ Assert.That(restructions.Single().ActionType, Is.EqualTo(TreeItemActionType.AppendChild));
+ });
+ });
+ });
+ }
+}
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/TransformerTests.cs b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/TransformerTests.cs
new file mode 100644
index 0000000..f1b024d
--- /dev/null
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/TransformerTests.cs
@@ -0,0 +1,135 @@
+//
+// Copyright (c) Heleonix - Hennadii Lutsyshyn. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the repository root for full license information.
+//
+
+namespace Heleonix.Docfx.Plugins.XmlDoc.Tests;
+
+using global::Docfx.Plugins;
+using Moq;
+
+///
+/// Tests the .
+///
+[ComponentTest(Type = typeof(Transformer))]
+public static class TransformerTests
+{
+ ///
+ /// Tests the .
+ ///
+ [MemberTest(Name = nameof(Transformer.Transform))]
+ public static void Transform()
+ {
+ var transformer = new Transformer();
+ FileModel model = null;
+ Mock hostMock = null;
+ var result = default(string);
+ Exception exception = null;
+
+ When("the method is called", () =>
+ {
+ Act(() =>
+ {
+ try
+ {
+ result = transformer.Transform(model, hostMock.Object);
+ }
+ catch (Exception ex)
+ {
+ exception = ex;
+ }
+ });
+
+ And("the template format is 'xslt'", () =>
+ {
+ Arrange(() =>
+ {
+ hostMock = new Mock();
+
+ model = new FileModel(
+ new FileAndType(Environment.CurrentDirectory, "Input.xsd", DocumentType.Article),
+ new Dictionary
+ {
+ { FileMetadata.TemplateKey, Path.Combine(Environment.CurrentDirectory, "Template.xslt") },
+ });
+ });
+
+ Should("successfully transform the xml-base file into markdown", () =>
+ {
+ Assert.That(result, Contains.Substring("Hx_NetBuild_ArtifactsDir"));
+ Assert.That(result, Contains.Substring("Hx_NetBuild_SlnFile"));
+ Assert.That(result, Contains.Substring("Hx_NetBuild_SnkFile"));
+ });
+ });
+
+ And("the template format is 'cshtml'", () =>
+ {
+ Arrange(() =>
+ {
+ model = new FileModel(
+ new FileAndType(Environment.CurrentDirectory, "Input.xsd", DocumentType.Article),
+ new Dictionary
+ {
+ { FileMetadata.TemplateKey, Path.Combine(Environment.CurrentDirectory, "Template.cshtml") },
+ });
+ });
+
+ Should("successfully transform the xml-base file into markdown", () =>
+ {
+ Assert.That(result, Contains.Substring("Hx_NetBuild_ArtifactsDir"));
+ Assert.That(result, Contains.Substring("Hx_NetBuild_SlnFile"));
+ Assert.That(result, Contains.Substring("Hx_NetBuild_SnkFile"));
+ });
+ });
+
+ And("the template format is not recognized", () =>
+ {
+ Arrange(() =>
+ {
+ model = new FileModel(
+ new FileAndType(Environment.CurrentDirectory, "Input.xsd", DocumentType.Article),
+ new Dictionary
+ {
+ { FileMetadata.TemplateKey, Path.Combine(Environment.CurrentDirectory, "Template.unknown") },
+ });
+
+ hostMock.Setup((IHostService hostService) => hostService.LogError(
+ It.Is(s => s == $"Unknown template file format: {Path.Combine(Environment.CurrentDirectory, "Template.unknown")}."),
+ It.Is(s => s == model.FileAndType.FullPath),
+ It.IsAny())).Verifiable();
+ });
+
+ Should("log an error and return null", () =>
+ {
+ Assert.That(result, Is.Null);
+ hostMock.Verify();
+ });
+ });
+
+ And("some exception is thrown during transformation", () =>
+ {
+ Arrange(() =>
+ {
+ model = new FileModel(
+ new FileAndType(Environment.CurrentDirectory, "Input.xsd", DocumentType.Article),
+ new Dictionary
+ {
+ { FileMetadata.TemplateKey, Path.Combine(Environment.CurrentDirectory, "NO_FILE.cshtml") },
+ });
+
+ hostMock.Setup((IHostService hostService) => hostService.LogError(
+ It.Is(s => s.Contains("FileNotFoundException")),
+ It.Is(s => s == model.FileAndType.FullPath),
+ It.IsAny())).Verifiable();
+ });
+
+ Should("log an error and throw exception", () =>
+ {
+ Assert.That(result, Is.Null);
+ Assert.That(exception, Is.Not.Null);
+ hostMock.Verify();
+ });
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/XmlDocBuildStepTests.cs b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/XmlDocBuildStepTests.cs
index aac62a8..1e51527 100644
--- a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/XmlDocBuildStepTests.cs
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/XmlDocBuildStepTests.cs
@@ -5,11 +5,15 @@
namespace Heleonix.Docfx.Plugins.XmlDoc.Tests;
+using global::Docfx.Common;
using global::Docfx.DataContracts.Common;
using global::Docfx.Plugins;
using Heleonix.Docfx.Plugins.XmlDoc;
+using Microsoft.CodeAnalysis;
using Moq;
+using NUnit.Framework.Constraints;
using System.Collections.Immutable;
+using System.Reflection;
///
/// Tests the .
@@ -23,33 +27,44 @@ public static class XmlDocBuildStepTests
[MemberTest(Name = nameof(XmlDocBuildStep.Prebuild))]
public static void Prebuild()
{
- var xmlDocBuildStep = new XmlDocBuildStep();
var hostMock = new Mock();
- var ft = new FileAndType(Environment.CurrentDirectory, "Input.xsd", DocumentType.Article);
+ var transformerMock = new Mock();
+ var headerHandlerMock = new Mock();
+ var tocHandlerMock = new Mock();
+ var xmlDocBuildStep = new XmlDocBuildStep
+ {
+ Transformer = transformerMock.Object,
+ HeaderHandler = headerHandlerMock.Object,
+ TocHandler = tocHandlerMock.Object,
+ };
+
+ var fileAndType = new FileAndType("X:/BaseDir", "file.xsd", DocumentType.Article);
ImmutableList models = null;
- string contentHtml = null;
- ImmutableDictionary yamlHeader = null;
XmlDocProcessor processor = null;
FileModel result = null;
- Dictionary metadata = null;
-
- metadata = new Dictionary
- {
- { FileMetadata.XsltKey, Path.Combine(Environment.CurrentDirectory, "Template.xslt") },
- };
Arrange(() =>
{
processor = new XmlDocProcessor();
- processor.GetProcessingPriority(ft);
+ processor.GetProcessingPriority(fileAndType);
- var model = processor.Load(ft, metadata.ToImmutableDictionary());
+ var metadata = new Dictionary
+ {
+ { FileMetadata.TemplateKey, Path.Combine(Environment.CurrentDirectory, "Template.xslt") },
+ };
+
+ var model = processor.Load(fileAndType, metadata.ToImmutableDictionary());
+
+ model.Uids = new[] { new UidDefinition("some.uid", model.LocalPathFromRoot) }.ToImmutableArray();
+
+ transformerMock.Setup((ITransformer t) => t.Transform(model, hostMock.Object))
+ .Returns("Transformed MD").Verifiable();
var markupResult = new MarkupResult
{
- Html = contentHtml,
- YamlHeader = yamlHeader,
+ Html = "Markup Html",
+ YamlHeader = new Dictionary().ToImmutableDictionary(),
LinkToFiles = new string[] { "link1" }.ToImmutableArray(),
FileLinkSources = new Dictionary>
{
@@ -62,12 +77,22 @@ public static void Prebuild()
}.ToImmutableDictionary(),
};
- models = new List { model }.ToImmutableList();
+ hostMock.SetupGet((IHostService hs) => hs.Processor)
+ .Returns(processor).Verifiable();
+ hostMock.Setup((IHostService hs) => hs.Markup("Transformed MD", fileAndType, false))
+ .Returns(markupResult).Verifiable();
- hostMock.SetupGet((IHostService hostService) => hostService.Processor).Returns(processor);
- hostMock.Setup((IHostService hostService) => hostService.Markup(
- It.Is(c => c.Contains("### Properties")), ft, false))
- .Returns(markupResult);
+ headerHandlerMock.Setup((IHeaderHandler hh) => hh.ExtractH1("Markup Html"))
+ .Returns(("h1", "h1 raw", "Conceptual Content")).Verifiable();
+ headerHandlerMock.Setup((IHeaderHandler hh) => hh.HandleYamlHeader(markupResult.YamlHeader, model))
+ .Verifiable();
+ headerHandlerMock.Setup((IHeaderHandler hh) => hh.GetTitle(model, markupResult.YamlHeader, "h1"))
+ .Returns("Some Title").Verifiable();
+
+ tocHandlerMock.Setup((ITocHandler th) => th.HandleTocRestructions(model, It.IsAny>()))
+ .Verifiable();
+
+ models = new List { model }.ToImmutableList();
});
When("the method is called", () =>
@@ -77,30 +102,39 @@ public static void Prebuild()
result = xmlDocBuildStep.Prebuild(models, hostMock.Object).Single();
});
- And("there is a full html content", () =>
+ And("the content file model should be pre-built", () =>
{
- contentHtml = "Heading
Content
";
-
- Should("generate html content", () =>
+ Should("prebuild the passed model", () =>
{
var content = result.Content as IDictionary;
- Assert.That(content[Constants.PropertyName.Conceptual], Contains.Substring("Content"));
- Assert.That(content[Constants.PropertyName.Title], Contains.Substring("Heading"));
- Assert.That(result.ManifestProperties.rawTitle, Is.Null);
+ Assert.That(content["rawTitle"], Is.EqualTo("h1 raw"));
+ Assert.That(result.ManifestProperties.rawTitle, Is.EqualTo("h1 raw"));
+ Assert.That(content[Constants.PropertyName.Conceptual], Is.EqualTo("Conceptual Content"));
+ Assert.That(content[Constants.PropertyName.Title], Is.EqualTo("Some Title"));
+
Assert.That(result.LinkToFiles, Contains.Item("link1"));
Assert.That(result.LinkToUids, Contains.Item("uid1"));
- Assert.That(result.FileLinkSources["key1"][0], Is.EqualTo("src1"));
- Assert.That(result.FileLinkSources["key2"][0], Is.EqualTo("src2"));
- Assert.That(result.Properties.XrefSpec, Is.Null);
+ Assert.That(result.FileLinkSources["key1"][0].SourceFile, Is.EqualTo("src1"));
+ Assert.That(result.UidLinkSources["key2"][0].SourceFile, Is.EqualTo("src2"));
+ Assert.That(result.Properties.XrefSpec.Uid, Is.EqualTo(result.Uids[0].Name));
+ Assert.That(result.Properties.XrefSpec.Name, Is.EqualTo("Some Title"));
+ Assert.That(
+ result.Properties.XrefSpec.Href,
+ Is.EqualTo((string)((RelativePath)result.File).GetPathFromWorkingFolder()));
+
+ hostMock.Verify();
+ transformerMock.Verify();
+ headerHandlerMock.Verify();
+ tocHandlerMock.Verify();
});
});
- And("there is an unrecognized content file", () =>
+ And("there is an unrecognized content file model", () =>
{
Arrange(() =>
{
- processor.ContentFiles.Remove(ft.File, out _);
+ processor.ContentFiles.Remove(fileAndType.File, out _);
});
Should("skip the unrecognized file", () =>
@@ -110,207 +144,6 @@ public static void Prebuild()
Assert.That(content[Constants.PropertyName.Conceptual], Is.Empty);
});
});
-
- And("Table Of Contents is specified in metadata to be added after Href item", () =>
- {
- metadata = new Dictionary
- {
- { FileMetadata.XsltKey, Path.Combine(Environment.CurrentDirectory, "Template.xslt") },
- {
- FileMetadata.TocKey,
- new Dictionary
- {
- { "action", "InsertAfter" },
- { "key", "~/articles/introduction.md" },
- }
- },
- { "_appName", "App Name" },
- { "_appTitle", "App Title" },
- { "_enableSearch", true },
- };
-
- Should("generate html content with TOC restructions", () =>
- {
- var content = result.Content as IDictionary;
-
- Assert.That(content[Constants.PropertyName.Conceptual], Contains.Substring("Content"));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].ActionType,
- Is.EqualTo(TreeItemActionType.InsertAfter));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].Key,
- Is.EqualTo("~/articles/introduction.md"));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].TypeOfKey,
- Is.EqualTo(TreeItemKeyType.TopicHref));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].RestructuredItems[0].Metadata["name"],
- Is.EqualTo("Heading"));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].RestructuredItems[0].Metadata["_appName"],
- Is.EqualTo("App Name"));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].RestructuredItems[0].Metadata["_appTitle"],
- Is.EqualTo("App Title"));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].RestructuredItems[0].Metadata["_enableSearch"],
- Is.True);
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].RestructuredItems[0].Metadata[Constants.PropertyName.Href],
- Is.EqualTo("~/articles/introduction.md"));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].RestructuredItems[0].Metadata[Constants.PropertyName.TopicHref],
- Is.EqualTo("~/articles/introduction.md"));
- });
- });
-
- And("Table Of Contents is specified in metadata to be added after Uid item", () =>
- {
- metadata = new Dictionary
- {
- { FileMetadata.XsltKey, Path.Combine(Environment.CurrentDirectory, "Template.xslt") },
- {
- FileMetadata.TocKey,
- new Dictionary
- {
- { "action", "InsertAfter" },
- { "key", "introduction" },
- }
- },
- { "_appName", "App Name" },
- { "_appTitle", "App Title" },
- { "_enableSearch", true },
- };
-
- Should("generate html content with TOC restructions", () =>
- {
- var content = result.Content as IDictionary;
-
- Assert.That(content[Constants.PropertyName.Conceptual], Contains.Substring("Content"));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].ActionType,
- Is.EqualTo(TreeItemActionType.InsertAfter));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].Key,
- Is.EqualTo("introduction"));
- Assert.That(
- hostMock.Object.TableOfContentRestructions[0].TypeOfKey,
- Is.EqualTo(TreeItemKeyType.TopicUid));
- });
- });
-
- And("the loaded html content is a fragment of html document", () =>
- {
- contentHtml = " Heading
Text
";
-
- Should("generate html content", () =>
- {
- var content = result.Content as IDictionary;
-
- Assert.That(content[Constants.PropertyName.Conceptual], Contains.Substring("Text"));
- Assert.That(content[Constants.PropertyName.Title], Contains.Substring("Heading"));
- });
-
- And("the html content fragment has a comment", () =>
- {
- contentHtml = "Heading
Text
";
-
- Should("generate html content", () =>
- {
- var content = result.Content as IDictionary;
-
- Assert.That(content[Constants.PropertyName.Conceptual], Contains.Substring("Text"));
- Assert.That(content[Constants.PropertyName.Title], Contains.Substring("Heading"));
- });
- });
- });
-
- And("there is no 'h1' html tag", () =>
- {
- contentHtml = "Text
";
-
- Should("generate html content with the file name as a title", () =>
- {
- var content = result.Content as IDictionary;
-
- Assert.That(content[Constants.PropertyName.Conceptual], Contains.Substring("Text"));
- Assert.That(content[Constants.PropertyName.Title], Contains.Substring("Input"));
- });
- });
-
- And("the 'h1' title is overwritten in metadata", () =>
- {
- metadata = new Dictionary
- {
- { FileMetadata.XsltKey, Path.Combine(Environment.CurrentDirectory, "Template.xslt") },
- { Constants.PropertyName.TitleOverwriteH1, "Header Overwrite" },
- };
-
- Should("generate html content with overridden title", () =>
- {
- var content = result.Content as IDictionary;
-
- Assert.That(content[Constants.PropertyName.Title], Is.EqualTo("Header Overwrite"));
- });
- });
-
- And("there the 'h1' title is overwritten in global metadata", () =>
- {
- metadata = new Dictionary
- {
- { FileMetadata.XsltKey, Path.Combine(Environment.CurrentDirectory, "Template.xslt") },
- { Constants.PropertyName.Title, "Global Header" },
- };
-
- Should("generate html content with overridden title", () =>
- {
- var content = result.Content as IDictionary;
-
- Assert.That(content[Constants.PropertyName.Title], Is.EqualTo("Global Header"));
- });
- });
-
- And("there is a yaml header specified", () =>
- {
- yamlHeader = new Dictionary
- {
- { Constants.PropertyName.Uid, "some-uid" },
- { Constants.PropertyName.DocumentType, "Conceptual" },
- { Constants.PropertyName.OutputFileName, "output-file-name.html" },
- { Constants.PropertyName.Title, "Some Title" },
- { "any_other_key", "any-other-value" },
- }.ToImmutableDictionary();
-
- Should("generate html content with properties specified in the yaml header", () =>
- {
- var content = result.Content as IDictionary;
-
- Assert.That(content[Constants.PropertyName.Uid], Contains.Substring("some-uid"));
- Assert.That(result.Uids[0].Name, Is.EqualTo("some-uid"));
- Assert.That(result.Uids[0].File, Is.EqualTo(models[0].LocalPathFromRoot));
-
- Assert.That(content[Constants.PropertyName.DocumentType], Is.EqualTo("Conceptual"));
- Assert.That(result.DocumentType, Is.EqualTo("Conceptual"));
-
- Assert.That(content[Constants.PropertyName.OutputFileName], Is.EqualTo("output-file-name.html"));
- Assert.That(result.File, Is.EqualTo("output-file-name.html"));
-
- Assert.That(content["any_other_key"], Is.EqualTo("any-other-value"));
- });
-
- And("the yaml header has incorrect output file name", () =>
- {
- yamlHeader = new Dictionary
- {
- { Constants.PropertyName.OutputFileName, "extra-path/output-file-name.html" },
- }.ToImmutableDictionary();
-
- Should("generate html content", () =>
- {
- Assert.That(result.File, Is.EqualTo(models[0].File));
- });
- });
- });
});
}
diff --git a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/XmlDocProcessorTests.cs b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/XmlDocProcessorTests.cs
index 2f5211b..e88dda0 100644
--- a/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/XmlDocProcessorTests.cs
+++ b/test/Heleonix.Docfx.Plugins.XmlDoc.Tests/XmlDocProcessorTests.cs
@@ -201,7 +201,7 @@ public static void Load()
{
{ "key1", 111 },
{ "key2", 222 },
- { FileMetadata.XsltKey, "./relative/to/docfx-json/transform.xslt" },
+ { FileMetadata.TemplateKey, "./relative/to/docfx-json/transform.xslt" },
}.ToImmutableDictionary();
});
@@ -219,8 +219,8 @@ public static void Load()
Assert.That(content["key1"], Is.EqualTo(111));
Assert.That(content["key2"], Is.EqualTo(222));
Assert.That(
- content[FileMetadata.XsltKey],
- Is.EqualTo(Path.Combine(EnvironmentContext.BaseDirectory, metadata[FileMetadata.XsltKey] as string)));
+ content[FileMetadata.TemplateKey],
+ Is.EqualTo(Path.Combine(EnvironmentContext.BaseDirectory, metadata[FileMetadata.TemplateKey] as string)));
Assert.That(content[Constants.PropertyName.SystemKeys], Has.Length.EqualTo(8));