summaryrefslogtreecommitdiffstats
path: root/tools/Sandcastle/Source/BuildAssembler/BuildComponents/WdxResolveConceptualLinksComponent.cs
diff options
context:
space:
mode:
Diffstat (limited to 'tools/Sandcastle/Source/BuildAssembler/BuildComponents/WdxResolveConceptualLinksComponent.cs')
-rw-r--r--tools/Sandcastle/Source/BuildAssembler/BuildComponents/WdxResolveConceptualLinksComponent.cs296
1 files changed, 296 insertions, 0 deletions
diff --git a/tools/Sandcastle/Source/BuildAssembler/BuildComponents/WdxResolveConceptualLinksComponent.cs b/tools/Sandcastle/Source/BuildAssembler/BuildComponents/WdxResolveConceptualLinksComponent.cs
new file mode 100644
index 0000000..d404d28
--- /dev/null
+++ b/tools/Sandcastle/Source/BuildAssembler/BuildComponents/WdxResolveConceptualLinksComponent.cs
@@ -0,0 +1,296 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Xml;
+using System.Xml.XPath;
+using System.Text.RegularExpressions;
+using System.Web;
+
+
+namespace Microsoft.Ddue.Tools {
+
+ /// <summary>
+ /// WdxResolveConceptualLinksComponent handles conceptual links where the target is a GUID. All other kinds
+ /// of targets are considered invalid. NOTE: This is an experimental version for WebDocs.
+ /// </summary>
+ public class WdxResolveConceptualLinksComponent : BuildComponent {
+
+ const int MaxTargetCacheSize = 1000;
+
+ TargetSetList targetSets = new TargetSetList();
+ XPathExpression baseUrl;
+ string invalidLinkFormat = "<span class='nolink'>{1}</span>";
+ string brokenLinkFormat = "<a href='http://msdn2.microsoft.com/en-us/library/{0}'>{1}</a>";
+ string defaultFormat = "<a href='{0}'>{1}</a>";
+
+ public WdxResolveConceptualLinksComponent (BuildAssembler assembler, XPathNavigator configuration) : base(assembler, configuration) {
+ //System.Diagnostics.Debugger.Break();
+
+ string av; // temporary attribute values
+
+ // base-url is an xpath expression that is used to lookup the url that relative links need to be
+ // relative to. The lookup is done against the current document. This attribute is needed only if
+ // one of the targets uses relative links that are not in the current directory. If not specified,
+ // the target uses the url from the meta data unchanged.
+ av = configuration.GetAttribute("base-url", String.Empty);
+ if (!String.IsNullOrEmpty(av))
+ baseUrl = CompileXPathExpression(av);
+
+ // invalid-link-format specifies a string format to be used for invalid (target is not a valid GUID)
+ // links. The string formatter is called with parameter {0} set to the target attribute of the link,
+ // and parameter {1} set to the tag content from the source document. A reasonable default is used
+ // if the value is not specified.
+ av = configuration.GetAttribute("invalid-link-format", String.Empty);
+ if (!String.IsNullOrEmpty(av))
+ invalidLinkFormat = av;
+
+ // broken-link-format specifies a string format to be used for broken links (target GUID lookup
+ // failed in all targets). The string formatter is called with parameter {0} set to the target attribute
+ // of the link, and parameter {1} set to the tag content from the source document. A reasonable
+ // default is used if the value is not specified.
+ av = configuration.GetAttribute("broken-link-format", String.Empty);
+ if (!String.IsNullOrEmpty(av))
+ brokenLinkFormat = av;
+
+ // <targets> specifies a lookup solution for each possible set of link targets. Each target must
+ // specify either a lookup file or error condition (invalid-link, broken-link).
+ XPathNodeIterator targetsNodes = configuration.Select("targets");
+ foreach (XPathNavigator targetsNode in targetsNodes) {
+
+ // lookup-file specifies the meta data file used for looking up URLs and titles. The value will
+ // go through environment variable expansion during setup and then through string formatting after
+ // computing the url, with parameter {0} set to the link target GUID. This attribute is required.
+ string lookupFile = targetsNode.GetAttribute("lookup-file", String.Empty);
+ if (string.IsNullOrEmpty(lookupFile))
+ WriteMessage(MessageLevel.Error, "Each target must have a lookup-file attribute.");
+ else
+ lookupFile = Environment.ExpandEnvironmentVariables(lookupFile);
+
+ // check-file-exists if specified ensures that the link target file exists; if it doesn't exist we
+ // take the broken link action.
+ string checkFileExists = targetsNode.GetAttribute("check-file-exists", String.Empty);
+ if (!String.IsNullOrEmpty(checkFileExists))
+ checkFileExists = Environment.ExpandEnvironmentVariables(checkFileExists);
+
+ // url is an xpath expression that is used to lookup the link url in the meta data file. The default
+ // value can be used to lookup the url in .cmp.xml files.
+ av = targetsNode.GetAttribute("url", String.Empty);
+ XPathExpression url = String.IsNullOrEmpty(av) ?
+ XPathExpression.Compile("concat(/metadata/topic/@id,'.htm')") :
+ XPathExpression.Compile(av);
+
+ // text is an xpath expression that is used to lookup the link text in the meta data file. The default
+ // value can be used to lookup the link text in .cmp.xml files.
+ av = targetsNode.GetAttribute("text", string.Empty);
+ XPathExpression text = String.IsNullOrEmpty(av) ?
+ XPathExpression.Compile("string(/metadata/topic/title)") :
+ XPathExpression.Compile(av);
+
+ // relative-url determines whether the links from this target set are relative to the current page
+ // and need to be adjusted to the base directory.
+ av = targetsNode.GetAttribute("relative-url", String.Empty);
+ bool relativeUrl = String.IsNullOrEmpty(av) ? false : Convert.ToBoolean(av);;
+
+ // format is a format string that is used to generate the link. Parameter {0} is the url;
+ // parameter {1} is the text. The default creates a standard HTML link.
+ string format = targetsNode.GetAttribute("format", String.Empty);
+ if (String.IsNullOrEmpty(format))
+ format = defaultFormat;
+
+ // target looks OK
+ targetSets.Add(new TargetSet(lookupFile, checkFileExists, url, text, relativeUrl, format));
+ }
+
+ WriteMessage(MessageLevel.Info, String.Format("Collected {0} targets directories.", targetSets.Count));
+ }
+
+ public override void Apply(XmlDocument document, string key) {
+ // Run through all conceptual nodes, make sure the target is a valid GUID, attempt to resolve the target
+ // to a link using one of the TargetSet definitions, and then replace the node with the result. Errors
+ // will be dealt with as follows: 1) bad target (GUID) -> output invalidLinkFormat; 2) broken link (cannot
+ // resolve target or the URL is empty) -> output brokenLinkFormat; 3) missing text -> just delete the node
+ // and don't output anything (presumably there's no point in creating a link that nobody can see). In all
+ // three cases we'll log the problem as a warning.
+ string docBaseUrl = baseUrl == null ? null : BuildComponentUtilities.EvalXPathExpr(document, baseUrl, "key", key);
+ XPathNavigator[] linkNodes = BuildComponentUtilities.ConvertNodeIteratorToArray(document.CreateNavigator().Select(conceptualLinks));
+ foreach (XPathNavigator node in linkNodes) {
+ string targetGuid = node.GetAttribute("target", String.Empty);
+ string url = targetGuid;
+ string text = node.ToString();
+ string format;
+ if (validGuid.IsMatch(url)) {
+ format = brokenLinkFormat;
+ Target t = targetSets.Lookup(targetGuid);
+ if (t == null) {
+ WriteMessage(MessageLevel.Warn, String.Format("Conceptual link not found in target sets; target={0}", targetGuid));
+ }
+ else {
+ if (!String.IsNullOrEmpty(t.Url)) {
+ format = t.TargetSet.Format;
+ url = (docBaseUrl != null && t.TargetSet.RelativeUrl) ?
+ BuildComponentUtilities.GetRelativePath(t.Url, docBaseUrl) : t.Url;
+ if (!String.IsNullOrEmpty(t.Text))
+ text = t.Text;
+ }
+ else
+ WriteMessage(MessageLevel.Warn, String.Format("Conceptual link found in target set, but meta data does not specify a url; target={0}", targetGuid));
+ }
+ }
+ else
+ format = invalidLinkFormat;
+
+ if (String.IsNullOrEmpty(text)) {
+ node.DeleteSelf();
+ WriteMessage(MessageLevel.Warn, String.Format("Skipping conceptual link without text; target={0}", url));
+ }
+ else {
+ node.OuterXml = String.Format(format, url, text);
+ }
+ }
+ }
+
+ private static XPathExpression conceptualLinks = XPathExpression.Compile("//conceptualLink");
+ private static Regex validGuid = new Regex(@"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", RegexOptions.Compiled);
+
+
+ #region HelperFunctions
+
+ public XPathExpression CompileXPathExpression(string xpath) {
+ XPathExpression expression = null;
+ try {
+ expression = XPathExpression.Compile(xpath);
+ }
+ catch (ArgumentException e) {
+ WriteMessage(MessageLevel.Error, String.Format("'{0}' is not a valid XPath expression. The error message is: {1}", xpath, e.Message));
+ }
+ catch (XPathException e) {
+ WriteMessage(MessageLevel.Error, String.Format("'{0}' is not a valid XPath expression. The error message is: {1}", xpath, e.Message));
+ }
+ return (expression);
+ }
+
+ #endregion HelperFunctions
+
+
+ #region DataStructures
+
+ //
+ // Internal data structures to support WdxResolveConceptualLinksComponent
+ //
+
+ class TargetSet {
+ string lookupFile;
+ string checkFileExists;
+ XPathExpression url;
+ XPathExpression text;
+
+ bool relativeUrl;
+ public bool RelativeUrl {
+ get { return relativeUrl; }
+ }
+
+ string format;
+ public string Format {
+ get { return format; }
+ }
+
+ public TargetSet(string lookupFile, string checkFileExists, XPathExpression url, XPathExpression text, bool relativeUrl, string format) {
+ if (lookupFile == null)
+ throw new ArgumentNullException("lookupFile");
+ this.lookupFile = lookupFile;
+
+ this.checkFileExists = checkFileExists;
+
+ if (url == null)
+ throw new ArgumentNullException("url");
+ this.url = url;
+
+ if (text == null)
+ throw new ArgumentNullException("text");
+ this.text = text;
+
+ this.relativeUrl = relativeUrl;
+
+ if (format == null)
+ throw new ArgumentNullException("format");
+ this.format = format;
+ }
+
+ public Target Lookup(string targetGuid) {
+ string lookupFilePathName = String.Format(lookupFile, targetGuid);
+ if (File.Exists(lookupFilePathName) &&
+ (checkFileExists == null || File.Exists(String.Format(checkFileExists, targetGuid)))) {
+ XPathDocument document = new XPathDocument(lookupFilePathName);
+ return new Target(this,
+ (string)document.CreateNavigator().Evaluate(url),
+ (string)document.CreateNavigator().Evaluate(text));
+ }
+ return null;
+ }
+ }
+
+ class TargetSetList {
+ List<TargetSet> targetSets = new List<TargetSet>();
+ Dictionary<string, Target> targetCache = new Dictionary<string, Target>();
+
+ public TargetSetList() {
+ }
+
+ public void Add(TargetSet targetSet) {
+ if (targetSet == null)
+ throw new ArgumentNullException("targetSet");
+ this.targetSets.Add(targetSet);
+ }
+
+ public int Count {
+ get { return targetSets.Count; }
+ }
+
+ public Target Lookup(string targetGuid) {
+ Target t = null;
+ if (!targetCache.TryGetValue(targetGuid, out t)) {
+ foreach (TargetSet ts in targetSets) {
+ t = ts.Lookup(targetGuid);
+ if (t != null) {
+ if (targetCache.Count >= MaxTargetCacheSize)
+ targetCache.Clear();
+ targetCache.Add(targetGuid, t);
+ break;
+ }
+ }
+ }
+ return t;
+ }
+ }
+
+ class Target {
+ TargetSet targetSet;
+ public TargetSet TargetSet {
+ get { return targetSet; }
+ }
+
+ string url;
+ public string Url {
+ get { return url; }
+ }
+
+ string text;
+ public string Text {
+ get { return text; }
+ }
+
+ public Target(TargetSet targetSet, string url, string text) {
+ if (targetSet == null)
+ throw new ArgumentNullException("targetSet");
+ this.targetSet = targetSet;
+ this.url = url;
+ this.text = text;
+ }
+ }
+
+ #endregion DataStructures
+ }
+} \ No newline at end of file