summaryrefslogtreecommitdiffstats
path: root/tools/Sandcastle/Source/BuildAssembler/BuildComponents/ResolveConceptualLinksComponent.cs
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2009-10-31 16:27:49 -0700
committerAndrew Arnott <andrewarnott@gmail.com>2009-10-31 16:27:49 -0700
commit55d6e4be43225184f9a656e6d9cc8fc1e8e7387b (patch)
treec382ef957455af8a39f80789dff9b112e97c2fe6 /tools/Sandcastle/Source/BuildAssembler/BuildComponents/ResolveConceptualLinksComponent.cs
parent07a8ecbc4fc50d11179f7e0b3fadb9f2be430ae5 (diff)
parentebf5efb48cddbfaf08aec24117b7c5f9626f1c02 (diff)
downloadDotNetOpenAuth-55d6e4be43225184f9a656e6d9cc8fc1e8e7387b.zip
DotNetOpenAuth-55d6e4be43225184f9a656e6d9cc8fc1e8e7387b.tar.gz
DotNetOpenAuth-55d6e4be43225184f9a656e6d9cc8fc1e8e7387b.tar.bz2
Merge branch 'contracts'
Diffstat (limited to 'tools/Sandcastle/Source/BuildAssembler/BuildComponents/ResolveConceptualLinksComponent.cs')
-rw-r--r--tools/Sandcastle/Source/BuildAssembler/BuildComponents/ResolveConceptualLinksComponent.cs448
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