diff options
Diffstat (limited to 'tools/Sandcastle/Source/BuildAssembler/BuildComponents/WdxResolveConceptualLinksComponent.cs')
-rw-r--r-- | tools/Sandcastle/Source/BuildAssembler/BuildComponents/WdxResolveConceptualLinksComponent.cs | 296 |
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 |