diff options
Diffstat (limited to 'tools/Sandcastle/Source/BuildAssembler/BuildComponents/ResolveConceptualLinksComponent.cs')
-rw-r--r-- | tools/Sandcastle/Source/BuildAssembler/BuildComponents/ResolveConceptualLinksComponent.cs | 448 |
1 files changed, 448 insertions, 0 deletions
diff --git a/tools/Sandcastle/Source/BuildAssembler/BuildComponents/ResolveConceptualLinksComponent.cs b/tools/Sandcastle/Source/BuildAssembler/BuildComponents/ResolveConceptualLinksComponent.cs new file mode 100644 index 0000000..2531e23 --- /dev/null +++ b/tools/Sandcastle/Source/BuildAssembler/BuildComponents/ResolveConceptualLinksComponent.cs @@ -0,0 +1,448 @@ +// 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. + +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 { + + public class ResolveConceptualLinksComponent : BuildComponent { + + // Instantiation logic + + public ResolveConceptualLinksComponent (BuildAssembler assembler, XPathNavigator configuration) : base(assembler, configuration) { + + string showBrokenLinkTextValue = configuration.GetAttribute("showBrokenLinkText", String.Empty); + if (!String.IsNullOrEmpty(showBrokenLinkTextValue)) showBrokenLinkText = Convert.ToBoolean(showBrokenLinkTextValue); + + XPathNodeIterator targetsNodes = configuration.Select("targets"); + foreach (XPathNavigator targetsNode in targetsNodes) { + + // the base directory containing target; required + string baseValue = targetsNode.GetAttribute("base", String.Empty); + if (String.IsNullOrEmpty(baseValue)) WriteMessage(MessageLevel.Error, "Every targets element must have a base attribute that specifies the path to a directory of target metadata files."); + baseValue = Environment.ExpandEnvironmentVariables(baseValue); + if (!Directory.Exists(baseValue)) WriteMessage(MessageLevel.Error, String.Format("The specified target metadata directory '{0}' does not exist.", baseValue)); + + // an xpath expression to construct a file name + // (not currently used; pattern is hard-coded to $target.cmp.xml + string filesValue = targetsNode.GetAttribute("files", String.Empty); + + // an xpath expression to construct a url + string urlValue = targetsNode.GetAttribute("url", String.Empty); + XPathExpression urlExpression; + if (String.IsNullOrEmpty(urlValue)) { + urlExpression = XPathExpression.Compile("concat(/metadata/topic/@id,'.htm')"); + } else { + urlExpression = CompileXPathExpression(urlValue); + } + + // an xpath expression to construct link text + string textValue = targetsNode.GetAttribute("text", String.Empty); + XPathExpression textExpression; + if (String.IsNullOrEmpty(textValue)) { + textExpression = XPathExpression.Compile("string(/metadata/topic/title)"); + } else { + textExpression = CompileXPathExpression(textValue); + } + + // the type of link to create to targets found in the directory; required + string typeValue = targetsNode.GetAttribute("type", String.Empty); + if (String.IsNullOrEmpty(typeValue)) WriteMessage(MessageLevel.Error, "Every targets element must have a type attribute that specifies what kind of link to create to targets found in that directory."); + + // convert the link type to an enumeration member + LinkType type = LinkType.None; + try { + type = (LinkType) Enum.Parse(typeof(LinkType), typeValue, true); + } catch (ArgumentException) { + WriteMessage(MessageLevel.Error, String.Format("'{0}' is not a valid link type.", typeValue)); + } + + // we have all the required information; create a TargetDirectory and add it to our collection + TargetDirectory targetDirectory = new TargetDirectory(baseValue, urlExpression, textExpression, type); + targetDirectories.Add(targetDirectory); + + } + + WriteMessage(MessageLevel.Info, String.Format("Collected {0} targets directories.", targetDirectories.Count)); + + } + + private 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); + } + + // Conceptual link resolution logic + + public override void Apply (XmlDocument document, string key) { + ResolveConceptualLinks(document, key); + } + + private bool showBrokenLinkText = false; + + private string BrokenLinkDisplayText (string target, string text) { + if (showBrokenLinkText) { + return(String.Format("{0}", text)); + } else { + return(String.Format("[{0}]", target)); + } + } + + private TargetDirectoryCollection targetDirectories = new TargetDirectoryCollection(); + + 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); + + private void ResolveConceptualLinks (XmlDocument document, string key) { + + // find links + XPathNodeIterator linkIterator = document.CreateNavigator().Select(conceptualLinks); + + // copy them to an array, because enumerating through an XPathNodeIterator + // fails when the nodes in it are altered + XPathNavigator[] linkNodes = BuildComponentUtilities.ConvertNodeIteratorToArray(linkIterator); + + foreach (XPathNavigator linkNode in linkNodes) { + + ConceptualLinkInfo link = ConceptualLinkInfo.Create(linkNode); + + // determine url, text, and link type + string url = null; + string text = null; + LinkType type = LinkType.None; + bool isValidLink = validGuid.IsMatch(link.Target); + if (isValidLink) { + // a valid link; try to fetch target info + TargetInfo target = GetTargetInfoFromCache(link.Target.ToLower()); + if (target == null) { + // no target found; issue warning, set link style to none, and text to in-source fall-back + //type = LinkType.None; + type = LinkType.Index; + text = BrokenLinkDisplayText(link.Target, link.Text); + WriteMessage(MessageLevel.Warn, String.Format("Unknown conceptual link target '{0}'.", link.Target)); + } else { + // found target; get url, text, and type from stored info + url = target.Url; + text = target.Text; + type = target.Type; + } + } else { + // not a valid link; issue warning, set link style to none, and text to invalid target + //type = LinkType.None; + type = LinkType.Index; + text = BrokenLinkDisplayText(link.Target, link.Text); + WriteMessage(MessageLevel.Warn, String.Format("Invalid conceptual link target '{0}'.", link.Target)); + } + + // write opening link tag and target info + XmlWriter writer = linkNode.InsertAfter(); + switch (type) { + case LinkType.None: + writer.WriteStartElement("span"); + writer.WriteAttributeString("class", "nolink"); + break; + case LinkType.Local: + writer.WriteStartElement("a"); + writer.WriteAttributeString("href", url); + break; + case LinkType.Index: + writer.WriteStartElement("mshelp", "link", "http://msdn.microsoft.com/mshelp"); + writer.WriteAttributeString("keywords", link.Target.ToLower()); + writer.WriteAttributeString("tabindex", "0"); + break; + } + + // write the link text + writer.WriteString(text); + + // write the closing link tag + writer.WriteEndElement(); + writer.Close(); + + // delete the original tag + linkNode.DeleteSelf(); + } + } + + + + + // a simple caching system for target names + + private TargetInfo GetTargetInfoFromCache (string target) { + + TargetInfo info; + if (!cache.TryGetValue(target, out info)) { + info = targetDirectories.GetTargetInfo(target + ".cmp.xml"); + + if (cache.Count >= cacheSize) cache.Clear(); + + cache.Add(target, info); + } + + return(info); + + } + + private static int cacheSize = 1000; + + private Dictionary<string,TargetInfo> cache = new Dictionary<string,TargetInfo>(cacheSize); + + //private CustomContext context = new CustomContext(); + + } + + // different types of links + + internal enum LinkType { + None, // not active + Local, // a href + Index // mshelp:link keyword + //Regex // regular expression with match/replace + } + + // a representation of a targets directory, along with all the assoicated expressions used to + // find target metadat files in it, and extract urls and link text from those files + + internal class TargetDirectory { + + private string directory; + + private XPathExpression fileExpression = XPathExpression.Compile("concat($target,'.cmp.htm')"); + + private XPathExpression urlExpression = XPathExpression.Compile("concat(/metadata/topic/@id,'.htm')"); + + private XPathExpression textExpression = XPathExpression.Compile("string(/metadata/topic/title)"); + + private LinkType type; + + public string Directory { + get { + return (directory); + } + } + + public XPathExpression UrlExpression { + get { + return (urlExpression); + } + } + + public XPathExpression TextExpression { + get { + return (textExpression); + } + } + + + public LinkType LinkType { + get { + return (type); + } + } + + public TargetDirectory (string directory, LinkType type) { + if (directory == null) throw new ArgumentNullException("directory"); + this.directory = directory; + this.type = type; + } + + public TargetDirectory (string directory, XPathExpression urlExpression, XPathExpression textExpression, LinkType type) { + if (directory == null) throw new ArgumentNullException("directory"); + if (urlExpression == null) throw new ArgumentNullException("urlExpression"); + if (textExpression == null) throw new ArgumentNullException("textExpression"); + this.directory = directory; + this.urlExpression = urlExpression; + this.textExpression = textExpression; + this.type = type; + } + + private XPathDocument GetDocument (string file) { + string path = Path.Combine(directory, file); + if (File.Exists(path)) { + XPathDocument document = new XPathDocument(path); + return (document); + } else { + return (null); + } + } + + public TargetInfo GetTargetInfo (string file) { + XPathDocument document = GetDocument(file); + if (document == null) { + return(null); + } else { + XPathNavigator context = document.CreateNavigator(); + + string url = context.Evaluate(urlExpression).ToString(); + string text = context.Evaluate(textExpression).ToString(); + TargetInfo info = new TargetInfo(url, text, type); + return(info); + } + } + + public TargetInfo GetTargetInfo (XPathNavigator linkNode, CustomContext context) { + + // compute the metadata file name to open + XPathExpression localFileExpression = fileExpression.Clone(); + localFileExpression.SetContext(context); + string file = linkNode.Evaluate(localFileExpression).ToString(); + if (String.IsNullOrEmpty(file)) return (null); + + // load the metadata file + XPathDocument metadataDocument = GetDocument(file); + if (metadataDocument == null) return (null); + + // querry the metadata file for the target url and link text + XPathNavigator metadataNode = metadataDocument.CreateNavigator(); + XPathExpression localUrlExpression = urlExpression.Clone(); + localUrlExpression.SetContext(context); + string url = metadataNode.Evaluate(localUrlExpression).ToString(); + XPathExpression localTextExpression = textExpression.Clone(); + localTextExpression.SetContext(context); + string text = metadataNode.Evaluate(localTextExpression).ToString(); + + // return this information + TargetInfo info = new TargetInfo(url, text, type); + return (info); + } + + + + } + + // our collection of targets directories + + internal class TargetDirectoryCollection { + + public TargetDirectoryCollection() {} + + private List<TargetDirectory> targetDirectories = new List<TargetDirectory>(); + + public int Count { + get { + return(targetDirectories.Count); + } + } + + public void Add (TargetDirectory targetDirectory) { + targetDirectories.Add(targetDirectory); + } + + public TargetInfo GetTargetInfo (string file) { + foreach (TargetDirectory targetDirectory in targetDirectories) { + TargetInfo info = targetDirectory.GetTargetInfo(file); + if (info != null) return (info); + } + return (null); + } + + public TargetInfo GetTargetInfo (XPathNavigator linkNode, CustomContext context) { + foreach (TargetDirectory targetDirectory in targetDirectories) { + TargetInfo info = targetDirectory.GetTargetInfo(linkNode, context); + if (info != null) return (info); + } + return (null); + } + + } + + // a representation of a resolved target, containing all the information necessary to actually write out the link + + internal class TargetInfo { + + private string url; + + private string text; + + private LinkType type; + + public string Url { + get { + return(url); + } + } + + public string Text { + get { + return(text); + } + } + + public LinkType Type { + get { + return(type); + } + } + + internal TargetInfo (string url, string text, LinkType type) { + if (url == null) throw new ArgumentNullException("url"); + if (text == null) throw new ArgumentNullException("url"); + this.url = url; + this.text = text; + this.type = type; + } + } + + // a representation of a conceptual link + + internal class ConceptualLinkInfo { + + private string target; + + private string text; + + private bool showText = false; + + public string Target { + get { + return (target); + } + } + + public string Text { + get { + return (text); + } + } + + public bool ShowText { + get { + return (showText); + } + } + + private ConceptualLinkInfo () { } + + public static ConceptualLinkInfo Create (XPathNavigator node) { + if (node == null) throw new ArgumentNullException("node"); + + ConceptualLinkInfo info = new ConceptualLinkInfo(); + + info.target = node.GetAttribute("target", String.Empty); + info.text = node.ToString(); + + return(info); + } + + } + +}
\ No newline at end of file |