-
Notifications
You must be signed in to change notification settings - Fork 365
/
MSHCComponent.cs
293 lines (243 loc) · 12.9 KB
/
MSHCComponent.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
//===============================================================================================================
// System : Sandcastle Build Components
// File : MSHCComponent.cs
// Note : Copyright 2010-2023 Microsoft Corporation
//
// This file contains a modified version of the original MSHCComponent that allows the inclusion of a sortOrder
// attribute on the table of contents file elements. This allows the sort order of the elements to be defined
// to set the proper placement of the TOC entries when parented to an entry outside of the help file and to
// parent the API content within a conceptual content folder.
//
// This code is published under the Microsoft Public License (Ms-PL). A copy of the license should be
// distributed with the code and can be found at the project website: https://GitHub.com/EWSoftware/SHFB. This
// notice and all copyright notices must remain intact in all applications, documentation, and source files.
//
// Change History
// 02/16/2012 - EFW - Merged my changes into the code
// 09/28/2012 - EFW - Changed "SelfBranded" to "Microsoft.Help.SelfBranded" for Help Viewer 2.0 support.
// Removed the ContentType metadata as it's output by the XSL transformations.
// Removed header bottom fix up code as it is handled in the XSL transformations and script.
// 12/23/2013 - EFW - Updated the build component to be discoverable via MEF
// 12/24/2015 - EFW - Updated to be thread safe
// 03/01/2022 - EFW - Removed all data island related code as the new presentation styles render the help viewer
// elements directly now.
//===============================================================================================================
// Ignore Spelling: Fehr
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using Sandcastle.Core.BuildAssembler;
using Sandcastle.Core.BuildAssembler.BuildComponent;
namespace Sandcastle.Tools.BuildComponents
{
/// <summary>
/// This class is a modified version of the original <c>MSHCComponent</c> that is used to add MS Help Viewer
/// meta data to the topics. This version allows the inclusion of a <c>sortOrder</c> attribute on the table
/// of contents file elements. This allows the sort order of the elements to be defined to set the proper
/// placement of the TOC entries when parented to an entry outside of the help file and to parent the API
/// content within a conceptual content folder.
/// </summary>
/// <remarks>The <c>sortOrder</c> attributes are optional. If not found, standard ordering is applied
/// starting from zero. If a <c>sortOrder</c> attribute is found, numbering starts from that value for the
/// associated topic and increments by one for all subsequent topics until another <c>sortOrder</c> attribute
/// is encountered or the end of the group is reached.</remarks>
/// <example>
/// <code language="xml" title="Example Component Configuration">
/// <component id="Microsoft Help Viewer Metadata Component">
/// <data self-branded="true" topic-version="100" toc-file="toc.xml"
/// toc-parent="" toc-parent-version="100" />
/// </component>
/// </code>
///
/// <code language="xml" title="Example toc.xml File">
/// <?xml version="1.0" encoding="utf-8"?>
/// <topics>
/// <!-- Sort our content below that of the parent node's existing sub-topics -->
/// <topic id="d4648875-d41a-783b-d5f4-638df39ee413" file="d4648875-d41a-783b-d5f4-638df39ee413" sortOrder="100">
/// <topic id="57f7aedc-17d3-4547-bdf9-5b468a08a1bc" file="57f7aedc-17d3-4547-bdf9-5b468a08a1bc" />
/// <topic id="0e6bbd29-775a-8deb-c4f5-5b1e63349ef1" file="0e6bbd29-775a-8deb-c4f5-5b1e63349ef1" />
/// <topic id="fcdfafc4-7625-f407-d8e9-ec006944e1d7" file="fcdfafc4-7625-f407-d8e9-ec006944e1d7" />
/// <!-- API content (7 namespaces, merged later) goes here and this topic follows it -->
/// <topic id="ce37cf86-fd95-49fc-b048-ba7d25d68d87" file="ce37cf86-fd95-49fc-b048-ba7d25d68d87" sortOrder="10">
/// </topic>
/// .
/// .
/// .
/// </topics>
/// </code>
/// </example>
public class MSHCComponent : BuildComponentCore
{
#region Build component factory for MEF
//=====================================================================
/// <summary>
/// This is used to create a new instance of the build component
/// </summary>
[BuildComponentExport("Microsoft Help Viewer Metadata Component")]
public sealed class Factory : BuildComponentFactory
{
/// <inheritdoc />
public override BuildComponentCore Create()
{
return new MSHCComponent(this.BuildAssembler);
}
}
#endregion
#region TOC information class
//=====================================================================
// TOC information of a document
private class TocInfo
{
public TocInfo(string parent, string parentVersion, int order)
{
this.Parent = parent;
this.ParentVersion = parentVersion;
this.Order = order;
}
public string Parent { get ; }
public string ParentVersion { get; }
public int Order { get; }
}
#endregion
#region Private data members
//=====================================================================
private string _topicVersion = "100", _tocParent = "-1", _tocParentVersion = "100", iconPath,
styleSheetPath, scriptPath;
private readonly Dictionary<string, TocInfo> _toc = new Dictionary<string, TocInfo>();
#endregion
#region Constructor
//=====================================================================
/// <summary>
/// Constructor
/// </summary>
/// <param name="buildAssembler">A reference to the build assembler</param>
protected MSHCComponent(IBuildAssembler buildAssembler) : base(buildAssembler)
{
}
#endregion
#region Method overrides
//=====================================================================
/// <inheritdoc />
public override void Initialize(XPathNavigator configuration)
{
if(configuration == null)
throw new ArgumentNullException(nameof(configuration));
iconPath = this.BuildAssembler.TopicTransformation.IconPath;
styleSheetPath = this.BuildAssembler.TopicTransformation.StyleSheetPath;
scriptPath = this.BuildAssembler.TopicTransformation.ScriptPath;
string tocFile = "toc.xml";
XPathNavigator data = configuration.SelectSingleNode("data");
if(data != null)
{
string value = data.GetAttribute("topic-version", String.Empty);
if(!String.IsNullOrEmpty(value))
_topicVersion = value;
value = data.GetAttribute("toc-parent", String.Empty);
if(!String.IsNullOrEmpty(value))
_tocParent = value;
value = data.GetAttribute("toc-parent-version", String.Empty);
if(!String.IsNullOrEmpty(value))
_tocParentVersion = value;
value = data.GetAttribute("toc-file", String.Empty);
if(!String.IsNullOrEmpty(value))
tocFile = value;
}
using(var reader = XmlReader.Create(Path.GetFullPath(Environment.ExpandEnvironmentVariables(tocFile)),
new XmlReaderSettings { CloseInput = true }))
{
XPathDocument document = new XPathDocument(reader);
XPathNavigator navigator = document.CreateNavigator();
LoadToc(navigator.SelectSingleNode("/topics"), _tocParent, _tocParentVersion);
}
}
/// <inheritdoc />
public override void Apply(XmlDocument document, string key)
{
if(document == null)
throw new ArgumentNullException(nameof(document));
XmlElement html = document.DocumentElement;
// Remove the relative path from src and href attribute values for icons, style sheets, and scripts
foreach(XmlNode srcHref in html.SelectNodes("//*[@src|@href]"))
{
var attr = srcHref.Attributes["src"] ?? srcHref.Attributes["href"];
if(attr.Value.StartsWith(iconPath, StringComparison.OrdinalIgnoreCase) ||
attr.Value.StartsWith(styleSheetPath, StringComparison.OrdinalIgnoreCase) ||
attr.Value.StartsWith(scriptPath, StringComparison.OrdinalIgnoreCase))
{
while(attr.Value.StartsWith("../", StringComparison.Ordinal))
attr.Value = attr.Value.Substring(3);
}
}
XmlNode head = html.SelectSingleNode("head");
if(head == null)
{
head = document.CreateElement("head");
if(!html.HasChildNodes)
html.AppendChild(head);
else
html.InsertBefore(head, html.FirstChild);
}
AddMHSMeta(head, "Microsoft.Help.TopicVersion", _topicVersion);
string id = null;
if(head.SelectSingleNode("meta[@name='Microsoft.Help.Id']") is XmlElement idElement)
id = idElement.GetAttribute("content");
if(id != null && _toc.TryGetValue(id, out TocInfo tocInfo))
{
AddMHSMeta(head, "Microsoft.Help.TocParent", tocInfo.Parent);
if(tocInfo.Parent != "-1")
AddMHSMeta(head, "Microsoft.Help.TOCParentTopicVersion", tocInfo.ParentVersion);
AddMHSMeta(head, "Microsoft.Help.TocOrder", tocInfo.Order.ToString(CultureInfo.InvariantCulture));
}
// Work around an odd bug in the help viewer. Depending on the ordering of the metadata and lack of
// whitespace, it can miss the title element and the TOC is blank. By adding a CR/LF before and
// after the title element, we can work around the issue.
XmlNode title = head.SelectSingleNode("title");
head.InsertBefore(document.CreateTextNode("\r\n"), title);
head.InsertAfter(document.CreateTextNode("\r\n"), title);
}
#endregion
#region Private helper methods
//=====================================================================
// Loads TOC structure from an XPathNavigator
private void LoadToc(XPathNavigator navigator, string parent, string parentVersion)
{
// EFW - Reworked to support sortOrder attribute
int sortOrder = 0;
XPathNodeIterator interator = navigator.SelectChildren("topic", String.Empty);
while(interator.MoveNext())
{
XPathNavigator current = interator.Current;
string id = current.GetAttribute("id", String.Empty);
// If a sort order is defined, start from that value
string order = current.GetAttribute("sortOrder", String.Empty);
if(!String.IsNullOrEmpty(order) && Int32.TryParse(order, out int tempOrder))
sortOrder = tempOrder;
if(!String.IsNullOrEmpty(id))
{
TocInfo info = new TocInfo(parent, parentVersion, sortOrder++);
// EFW - Work around a bug in Sandcastle that can result in duplicate IDs
// by using the indexer to add the topic rather than Add() which throws
// an exception when the duplicate is encountered.
_toc[id] = info;
LoadToc(current, id, _topicVersion);
}
}
}
// Adds Microsoft Help System meta data to the output document
private static void AddMHSMeta(XmlNode headElement, string name, string content)
{
if(!String.IsNullOrEmpty(content) && headElement.OwnerDocument.SelectSingleNode(
$"//meta[@name='{name}']") == null)
{
var elem = headElement.OwnerDocument.CreateElement("meta");
elem.SetAttribute("name", name);
elem.SetAttribute("content", content);
headElement.AppendChild(elem);
}
}
#endregion
}
}