summaryrefslogtreecommitdiffstats
path: root/tools/Sandcastle/Source/BuildAssembler/BuildComponents/WdxResolveConceptualLinksComponent.cs
blob: a827164eeef80281a2f0af9a234f231ddd21c85d (plain)
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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
// 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 {

    /// <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
    }
}