/// <inheritdoc /> public override void Apply(XmlDocument document, string key) { Target target = null, keyTarget; string msdnUrl = null; foreach (XPathNavigator linkNode in document.CreateNavigator().Select(referenceLinkExpression).ToArray()) { // Extract link information ReferenceLinkInfo link = new ReferenceLinkInfo(linkNode); // Determine target, link type, and display options string targetId = link.Target; DisplayOptions options = link.DisplayOptions; ReferenceLinkType type = ReferenceLinkType.None; if (String.IsNullOrWhiteSpace(targetId)) { this.WriteMessage(key, MessageLevel.Warn, "The target attribute is missing or has no " + "value. You have most likely omitted a cref attribute or left it blank on an XML " + "comments element such as see, seealso, or exception."); continue; } bool targetFound = targets.TryGetValue(targetId, out target, out type); // If it's an overload ID that wasn't found, it's possible that the overloads got excluded. // As such, see if we can find a match for a method using the same ID regardless of any // parameters. if (!targetFound && targetId.StartsWith("Overload:", StringComparison.Ordinal) || targetId.StartsWith("O:", StringComparison.Ordinal)) { string methodTargetId = "M:" + targetId.Substring(targetId.IndexOf(':') + 1); methodTargetId = targets.Keys.FirstOrDefault(k => k.StartsWith(methodTargetId)); if (methodTargetId != null) { targetId = methodTargetId; targetFound = targets.TryGetValue(targetId, out target, out type); options |= DisplayOptions.ShowParameters; // Don't use the content as it may not be appropriate for the method. The default is // "{0} Overload" which no longer applies. Instead we'll show the method name and // its parameters. if (link.DisplayTarget.Equals("format", StringComparison.OrdinalIgnoreCase)) { link.DisplayTarget = null; link.Contents = null; } } } // If not found and it starts with "System." or "Microsoft." we'll go with the assumption that // it's part of a Microsoft class library that is not part of the core framework but does have // documentation available on MSDN. Worst case it doesn't and we get an unresolved link warning // instead of an unknown reference target warning. if (!targetFound && ((targetId.Length > 9 && targetId.Substring(2).StartsWith("System.", StringComparison.Ordinal)) || (targetId.Length > 12 && targetId.Substring(2).StartsWith( "Microsoft.", StringComparison.Ordinal)))) { // Use the same link type as a core framework class targetFound = targets.TryGetValue("T:System.Object", out target, out type); // We don't have a target in this case so links to overloads pages won't work. Also note // that the link text will be generated from the ID which shouldn't make much of a difference // in most cases. If either case is an issue, the Additional Reference Links SHFB plug-in // can be used to generate valid link target data. target = null; } if (!targetFound) { // If not being rendered as a link, don't report a warning if (link.RenderAsLink && targetId != key) { this.WriteMessage(key, MessageLevel.Warn, "Unknown reference link target '{0}'.", targetId); } // !EFW - Turn off the Show Parameters option for unresolved elements except methods. If // not, it outputs an empty "()" after the member name which looks odd. if (targetId[0] != 'M') { options &= ~DisplayOptions.ShowParameters; } } else { // If overload is preferred and found, change targetId and make link options hide parameters if (link.PreferOverload && target != null) { bool isConversionOperator = false; MethodTarget method = target as MethodTarget; if (method != null) { isConversionOperator = method.IsConversionOperator; } MemberTarget member = target as MemberTarget; // If conversion operator is found, always link to individual topic if (member != null && !String.IsNullOrEmpty(member.OverloadId) && !isConversionOperator) { Target overloadTarget = targets[member.OverloadId]; if (overloadTarget != null) { target = overloadTarget; targetId = overloadTarget.Id; } } // If individual conversion operator is found, always display parameters if (isConversionOperator && member != null && !String.IsNullOrEmpty(member.OverloadId)) { options = options | DisplayOptions.ShowParameters; } else { options = options & ~DisplayOptions.ShowParameters; } } } // Suppress the link if so requested. Links to this page are not live. if (!link.RenderAsLink) { type = ReferenceLinkType.None; } else if (targetId == key) { type = ReferenceLinkType.Self; } else if (target != null && targets.TryGetValue(key, out keyTarget) && target.File == keyTarget.File) { type = ReferenceLinkType.Self; } // !EFW - Redirect enumeration fields to the containing enumerated type so that we // get a valid link target. Enum fields don't have a topic to themselves. if (type != ReferenceLinkType.None && type != ReferenceLinkType.Self && type != ReferenceLinkType.Local && targetId.StartsWith("F:", StringComparison.OrdinalIgnoreCase)) { MemberTarget member = target as MemberTarget; if (member != null) { SimpleTypeReference typeRef = member.ContainingType as SimpleTypeReference; if (typeRef != null && targets[typeRef.Id] is EnumerationTarget) { targetId = typeRef.Id; } } } // Get MSDN endpoint if needed if (type == ReferenceLinkType.Msdn) { if (msdnResolver != null && !msdnResolver.IsDisabled) { msdnUrl = msdnResolver.GetMsdnUrl(targetId); if (String.IsNullOrEmpty(msdnUrl)) { // If the web service failed, report the reason if (msdnResolver.IsDisabled) { this.WriteMessage(key, MessageLevel.Warn, "MSDN web service failed. No " + "further look ups will be performed for this build.\r\nReason: {0}", msdnResolver.DisabledReason); } else { this.WriteMessage(key, MessageLevel.Warn, "MSDN URL not found for target '{0}'.", targetId); } type = ReferenceLinkType.None; } } else { type = ReferenceLinkType.None; } } // Write opening link tag and target info XmlWriter writer = linkNode.InsertAfter(); switch (type) { case ReferenceLinkType.None: writer.WriteStartElement("span"); // If the link was intentionally suppressed, write it out as an identifier (i.e. links // in the syntax section). if (link.RenderAsLink) { writer.WriteAttributeString("class", "nolink"); } else { writer.WriteAttributeString("class", "identifier"); } break; case ReferenceLinkType.Self: writer.WriteStartElement("span"); writer.WriteAttributeString("class", "selflink"); break; case ReferenceLinkType.Local: // Format link with prefix and/or postfix string href = String.Format(CultureInfo.InvariantCulture, hrefFormat, target.File); // Make link relative, if we have a baseUrl if (baseUrl != null) { href = href.GetRelativePath(document.EvalXPathExpr(baseUrl, "key", key)); } writer.WriteStartElement("a"); writer.WriteAttributeString("href", href); break; case ReferenceLinkType.Msdn: writer.WriteStartElement("a"); writer.WriteAttributeString("href", msdnUrl); writer.WriteAttributeString("target", linkTarget); break; case ReferenceLinkType.Id: writer.WriteStartElement("a"); writer.WriteAttributeString("href", ("ms-xhelp:///?Id=" + targetId).Replace("#", "%23")); break; } // Write the link text if (String.IsNullOrEmpty(link.DisplayTarget)) { if (link.Contents == null) { if (target != null) { resolver.WriteTarget(target, options, writer); } else { Reference reference = TextReferenceUtilities.CreateReference(targetId); if (reference is InvalidReference) { this.WriteMessage(key, MessageLevel.Warn, "Invalid reference link target '{0}'.", targetId); } resolver.WriteReference(reference, options, writer); } } else { do { link.Contents.WriteSubtree(writer); } while(link.Contents.MoveToNext()); } } else { if (link.DisplayTarget.Equals("content", StringComparison.OrdinalIgnoreCase) && link.Contents != null) { // Use the contents as an XML representation of the display target Reference reference = XmlTargetDictionaryUtilities.CreateReference(link.Contents); resolver.WriteReference(reference, options, writer); } if (link.DisplayTarget.Equals("format", StringComparison.OrdinalIgnoreCase) && link.Contents != null) { // Use the contents as a format string for the display target string format = link.Contents.OuterXml; string input = null; using (StringWriter textStore = new StringWriter(CultureInfo.InvariantCulture)) { XmlWriterSettings settings = new XmlWriterSettings(); settings.ConformanceLevel = ConformanceLevel.Fragment; using (XmlWriter xmlStore = XmlWriter.Create(textStore, settings)) { if (target != null) { resolver.WriteTarget(target, options, xmlStore); } else { Reference reference = TextReferenceUtilities.CreateReference(targetId); resolver.WriteReference(reference, options, xmlStore); } } input = textStore.ToString(); } string output = String.Format(CultureInfo.InvariantCulture, format, input); XmlDocumentFragment fragment = document.CreateDocumentFragment(); fragment.InnerXml = output; fragment.WriteTo(writer); } else if (link.DisplayTarget.Equals("extension", StringComparison.OrdinalIgnoreCase) && link.Contents != null) { Reference extMethodReference = XmlTargetDictionaryUtilities.CreateExtensionMethodReference(link.Contents); resolver.WriteReference(extMethodReference, options, writer); } else { // Use the display target value as a CER for the display target TextReferenceUtilities.SetGenericContext(key); Reference reference = TextReferenceUtilities.CreateReference(link.DisplayTarget); resolver.WriteReference(reference, options, writer); } } // Write the closing link tag writer.WriteEndElement(); writer.Close(); // Delete the original tag linkNode.DeleteSelf(); } }
/// <summary> /// This is overridden to load the SQL dictionary in a thread-safe manner /// </summary> /// <param name="maxDegreeOfParallelism">This can be used to override the maximum degree of parallelism. /// By default, it is -1 to allow as many threads as possible.</param> protected override void LoadTargetDictionary(int maxDegreeOfParallelism = -1) { var namespaceFileFilter = this.NamespaceFileFilter; Parallel.ForEach(Directory.EnumerateFiles(this.DirectoryPath, this.FilePattern, this.Recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly), new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, file => { using (SqlConnection cn = new SqlConnection(connectionString)) using (SqlCommand cmd = new SqlCommand()) { cn.Open(); cmd.Connection = cn; cmd.Parameters.Add(new SqlParameter("@key", SqlDbType.VarChar, 768)); cmd.CommandText = String.Format(CultureInfo.InvariantCulture, "Select TargetKey From Targets Where GroupId = '{0}' And TargetKey = @key", base.DictionaryId); cmd.Parameters[0].Value = "N:" + Path.GetFileNameWithoutExtension(file); // Skip the file if not in a defined filter or if it's already in the dictionary if ((namespaceFileFilter.Count != 0 && !namespaceFileFilter.Contains(Path.GetFileName(file))) || cmd.ExecuteScalar() != null) { return; } this.BuildComponent.WriteMessage(MessageLevel.Info, "Indexing targets in {0}", file); cmd.CommandText = String.Format(CultureInfo.InvariantCulture, "IF NOT EXISTS(Select * From Targets Where GroupId = '{0}' And TargetKey = @key) " + "Insert Targets (GroupId, TargetKey, TargetValue) Values ('{0}', @key, @value) " + "ELSE Update Targets Set TargetValue = @value Where GroupId = '{0}' " + "And TargetKey = @key", base.DictionaryId); cmd.Parameters.Add(new SqlParameter("@value", SqlDbType.VarBinary)); BinaryFormatter bf = new BinaryFormatter { Context = new StreamingContext(StreamingContextStates.Persistence) }; try { XPathDocument document = new XPathDocument(file); foreach (var t in XmlTargetDictionaryUtilities.EnumerateTargets(document.CreateNavigator())) { cmd.Parameters[0].Value = t.Id; using (MemoryStream ms = new MemoryStream()) { bf.Serialize(ms, t); cmd.Parameters[1].Value = ms.GetBuffer(); cmd.ExecuteNonQuery(); } // Enumeration targets have members we need to add too. We can't use the // Target.Add() method here so we must do it manually. EnumerationTarget et = t as EnumerationTarget; if (et != null) { foreach (var el in et.Elements) { cmd.Parameters[0].Value = el.Id; using (MemoryStream ms = new MemoryStream()) { bf.Serialize(ms, el); cmd.Parameters[1].Value = ms.GetBuffer(); cmd.ExecuteNonQuery(); } } } } } catch (XmlSchemaException e) { throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "The reference targets file '{0}' is not valid. The error message is: {1}", file, e.GetExceptionMessage())); } catch (XmlException e) { throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "The reference targets file '{0}' is not well-formed XML. The error message is: {1}", file, e.GetExceptionMessage())); } catch (IOException e) { throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "An access error occured while opening the reference targets file '{0}'. The error " + "message is: {1}", file, e.GetExceptionMessage())); } } }); }