/
BuildComponentUtilities.cs
238 lines (199 loc) · 10.2 KB
/
BuildComponentUtilities.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
// Copyright © Microsoft Corporation.
// This source file is subject to the Microsoft Permissive License.
// See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx.
// All other rights reserved.
// Change History
// 12/23/2012 - EFW - Updated the utility methods to act as extension methods. Moved IsLegalXmlText() here
// as it was duplicated in the ExampleComponent and SnippetComponent and replaced it with a modified version
// from MRefBuilder.
// 12/21/2013 - EFW - Moved class to Sandcastle.Core assembly
// Ignore Spelling: xxx aaa bbb
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
namespace Sandcastle.Core.BuildAssembler
{
/// <summary>
/// This class contains a set of utility extension methods that can be used by build components
/// </summary>
public static class BuildComponentUtilities
{
/// <summary>
/// This is used to get the message strings from an exception and any of its inner exceptions
/// </summary>
/// <param name="exception">The exception from which to get the message</param>
/// <returns>The exception message along with any inner exception messages</returns>
/// <remarks><see cref="XmlException"/> and <see cref="XsltException"/> messages will be returned with
/// line number, line position, and source URI information.</remarks>
/// <exception cref="ArgumentNullException">This is thrown if the <paramref name="exception"/> argument
/// is null</exception>
public static string GetExceptionMessage(this Exception exception)
{
if(exception == null)
throw new ArgumentNullException(nameof(exception));
if(exception is AggregateException)
exception = exception.InnerException;
string message = exception.Message;
if(exception is XmlException xmlE)
message = String.Format(CultureInfo.CurrentCulture, "{0} (Line Number: {1}; Line Position: " +
"{2}; Source URI: '{3}')", message, xmlE.LineNumber, xmlE.LinePosition, xmlE.SourceUri);
if(exception is XsltException xslE)
message = String.Format(CultureInfo.CurrentCulture, "{0} (Line Number: {1}; Line Position: " +
"{2}; Source URI: '{3}')", message, xslE.LineNumber, xslE.LinePosition, xslE.SourceUri);
if(exception.InnerException != null)
message = String.Format(CultureInfo.CurrentCulture, "{0}\r\nInner Exception: {1}", message,
exception.InnerException.GetExceptionMessage());
return message;
}
/// <summary>
/// This is used to get the inner XML of a node without changing the spacing
/// </summary>
/// <param name="node">The node from which to get the inner XML</param>
/// <returns>The inner XML as a string with its spacing preserved</returns>
/// <exception cref="ArgumentNullException">This is thrown if the <paramref name="node"/> parameter
/// is null.</exception>
public static string GetInnerXml(this XPathNavigator node)
{
if(node == null)
throw new ArgumentNullException(nameof(node));
// Clone the node so that we don't change the input
XPathNavigator current = node.Clone();
XmlWriterSettings settings = new XmlWriterSettings
{
ConformanceLevel = ConformanceLevel.Fragment,
OmitXmlDeclaration = true
};
StringBuilder builder = new StringBuilder();
using(XmlWriter writer = XmlWriter.Create(builder, settings))
{
// write the output
bool writing = current.MoveToFirstChild();
while(writing)
{
current.WriteSubtree(writer);
writing = current.MoveToNext();
}
}
return builder.ToString();
}
/// <summary>
/// Convert an XPath node iterator to an array
/// </summary>
/// <param name="iterator">The XPath iterator to convert to an array</param>
/// <returns>An array with the cloned nodes from the iterator</returns>
public static XPathNavigator[] ToArray(this XPathNodeIterator iterator)
{
if(iterator == null)
throw new ArgumentNullException(nameof(iterator));
XPathNavigator[] result = new XPathNavigator[iterator.Count];
for(int i = 0; i < result.Length; i++)
{
iterator.MoveNext();
// Clone is required or all entries will equal Current!
result[i] = iterator.Current.Clone();
}
return result;
}
/// <summary>
/// This is used to get the string result from evaluating an XPath expression against the given
/// document and context.
/// </summary>
/// <param name="document">The document to use</param>
/// <param name="expression">The XPath expression to evaluate</param>
/// <param name="context">The context to use</param>
/// <returns>The evaluated expression result</returns>
/// <overloads>There are two overloads for this method</overloads>
public static string EvalXPathExpr(this IXPathNavigable document, XPathExpression expression,
CustomContext context)
{
if(document == null)
throw new ArgumentNullException(nameof(document));
if(expression == null)
throw new ArgumentNullException(nameof(expression));
XPathExpression t = expression.Clone();
t.SetContext(context);
return document.CreateNavigator().Evaluate(t).ToString();
}
/// <summary>
/// This is used to get the string result from evaluating an XPath expression against the given document
/// and a context created from a set of key/value pairs.
/// </summary>
/// <param name="document">The document to use</param>
/// <param name="expression">The XPath expression to evaluate</param>
/// <param name="keyValuePairs">A set of key/value pairs to use when creating the context</param>
/// <returns>The evaluated expression result</returns>
/// <example>
/// <code language="cs">
/// string result = document.EvalXPathExpr("concat($key, '.htm')", "key", "filename");
/// </code>
/// </example>
/// <exception cref="ArgumentException">This is thrown if the <paramref name="keyValuePairs"/>
/// parameter contains an odd number of parameters.</exception>
public static string EvalXPathExpr(this IXPathNavigable document, XPathExpression expression,
params string[] keyValuePairs)
{
CustomContext cc = new CustomContext();
if(keyValuePairs.Length % 2 != 0)
throw new ArgumentException("There must be a value for every key name specified", nameof(keyValuePairs));
for(int i = 0; i < keyValuePairs.Length; i += 2)
cc[keyValuePairs[i]] = keyValuePairs[i + 1];
return document.EvalXPathExpr(expression, cc);
}
/// <summary>
/// This returns the path argument adjusted to be relative to the base path. Absolute path names will
/// be returned unchanged.
/// </summary>
/// <param name="path">The path to adjust including the filename</param>
/// <param name="basePath">The base path to use including the filename</param>
/// <returns>The path argument as a path relative to the given base path</returns>
/// <example>
/// <code language="none" title=" ">
/// path: "xxx/aaa/target.html"
/// basePath: "xxx/bbb/source.html"
/// result: "../aaa/target.html"
/// </code>
/// </example>
/// <remarks>This assumes that the path separator is "/" and that both paths include a filename</remarks>
public static string GetRelativePath(this string path, string basePath)
{
// Ignore absolute path names and an empty basePath
if(!String.IsNullOrEmpty(path) && path[0] != '/' && !String.IsNullOrEmpty(basePath))
{
List<string> pathParts = new List<string>(path.Split('/'));
List<string> basePathParts = new List<string>(basePath.Split('/'));
// Remove the base path file name
if(basePathParts.Count > 0)
basePathParts.RemoveAt(basePathParts.Count - 1);
// Strip common base path bits
while(pathParts.Count > 0 && basePathParts.Count > 0 &&
pathParts[0].Equals(basePathParts[0], StringComparison.OrdinalIgnoreCase))
{
pathParts.RemoveAt(0);
basePathParts.RemoveAt(0);
}
// Move up one level for each remaining base path part
for(int i = 0; i < basePathParts.Count; i++)
pathParts.Insert(0, "..");
path = String.Join("/", pathParts);
}
return path;
}
private static readonly Regex reAllXmlChars = new Regex("[\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]");
/// <summary>
/// This is used to confirm that the specified text only contains legal XML characters
/// </summary>
/// <param name="text">The text to check</param>
/// <returns>True if all characters are legal XML characters, false if not</returns>
public static bool IsLegalXmlText(this string text)
{
if(String.IsNullOrEmpty(text))
return true;
return reAllXmlChars.IsMatch(text);
}
}
}