/// <summary> /// Merge the conceptual topic IDs into the BuildAssembler manifest file. /// </summary> /// <param name="builder">The build process</param> private void MergeConceptualManifest(BuildProcess builder) { XmlWriterSettings settings = new XmlWriterSettings(); XmlWriter writer = null; string conceptualManifest = builder.WorkingFolder + "ConceptualManifest.xml", referenceManifest = builder.WorkingFolder + "manifest.xml"; builder.ReportProgress(" Merging topic IDs into manifest.xml"); try { settings.Indent = true; settings.CloseOutput = true; writer = XmlWriter.Create(conceptualManifest, settings); writer.WriteStartDocument(); writer.WriteStartElement("topics"); foreach (TopicCollection tc in topics) { foreach (Topic t in tc) { t.WriteManifest(writer, builder); } } if (File.Exists(referenceManifest)) { foreach (var topic in ComponentUtilities.XmlStreamAxis(referenceManifest, "topic")) { writer.WriteStartElement("topic"); foreach (var attr in topic.Attributes()) { writer.WriteAttributeString(attr.Name.LocalName, attr.Value); } writer.WriteEndElement(); } } writer.WriteEndElement(); // </topics> writer.WriteEndDocument(); } finally { if (writer != null) { writer.Close(); } if (File.Exists(referenceManifest)) { File.Copy(referenceManifest, Path.ChangeExtension(referenceManifest, "old"), true); } File.Copy(conceptualManifest, referenceManifest, true); File.Delete(conceptualManifest); } }
//===================================================================== /// <summary> /// This is used to generate the API topic manifest /// </summary> private void GenerateApiTopicManifest() { this.ReportProgress(BuildStep.GenerateApiTopicManifest, "Generating API topic manifest..."); if (!this.ExecutePlugIns(ExecutionBehaviors.InsteadOf)) { this.ExecutePlugIns(ExecutionBehaviors.Before); using (XmlWriter writer = XmlWriter.Create(Path.Combine(this.WorkingFolder, "manifest.xml"), new XmlWriterSettings { Indent = true })) { writer.WriteStartDocument(); writer.WriteStartElement("topics"); foreach (var api in ComponentUtilities.XmlStreamAxis(reflectionFile, "api")) { // Skip elements that should not have a topic generated (enumerated type fields for example) if (api.Element("topicdata")?.Attribute("notopic") == null) { writer.WriteStartElement("topic"); writer.WriteAttributeString("id", api.Attribute("id").Value); writer.WriteAttributeString("type", "API"); writer.WriteEndElement(); } } writer.WriteEndElement(); } this.ExecutePlugIns(ExecutionBehaviors.After); } }
//===================================================================== /// <summary> /// This is used to get an enumerable list of unique namespaces from the given reflection data file /// </summary> /// <param name="reflectionInfoFile">The reflection data file to search for namespaces</param> /// <param name="validNamespaces">An enumerable list of valid namespaces</param> /// <returns>An enumerable list of unique namespaces</returns> public static IEnumerable <string> GetReferencedNamespaces(string reflectionInfoFile, IEnumerable <string> validNamespaces) { HashSet <string> seenNamespaces = new HashSet <string>(); string ns; // Find all type references and extract the namespace from them. This is a rather brute force way // of doing it but the type element can appear in various places. This way we find them all. // Examples: ancestors/type/@api, returns/type/@api, parameter/type/@api, // parameter/referenceTo/type/@api, attributes/attribute/argument/type/@api, // returns/type/specialization/type/@api, containers/type/@api, overrides/member/type/@api var typeRefs = ComponentUtilities.XmlStreamAxis(reflectionInfoFile, "type").Select( el => el.Attribute("api").Value); foreach (string typeName in typeRefs) { if (typeName.Length > 2 && typeName.IndexOf('.') != -1) { ns = typeName.Substring(2, typeName.LastIndexOf('.') - 2); if (validNamespaces.Contains(ns) && !seenNamespaces.Contains(ns)) { seenNamespaces.Add(ns); yield return(ns); } } } }
/// <summary> /// This is used to get an enumerable list of unique namespaces referenced in the XML comments files of /// the given set of namespaces. /// </summary> /// <param name="language">The language to use when locating the XML comments files</param> /// <param name="searchNamespaces">An enumerable list of namespaces to search</param> /// <param name="validNamespaces">An enumerable list of valid namespaces</param> /// <returns>An enumerable list of unique namespaces in the related XML comments files</returns> public IEnumerable <string> GetReferencedNamespaces(CultureInfo language, IEnumerable <string> searchNamespaces, IEnumerable <string> validNamespaces) { HashSet <string> seenNamespaces = new HashSet <string>(); string ns; foreach (string path in this.CommentsFileLocations(language)) { foreach (string file in Directory.EnumerateFiles(Path.GetDirectoryName(path), Path.GetFileName(path)).Where(f => searchNamespaces.Contains(Path.GetFileNameWithoutExtension(f)))) { // Find all comments elements with a reference. XML comments files may be ill-formed so // ignore any elements without a cref attribute. var crefs = ComponentUtilities.XmlStreamAxis(file, new[] { "event", "exception", "inheritdoc", "permission", "see", "seealso" }).Select( el => (string)el.Attribute("cref")).Where(c => c != null); foreach (string refId in crefs) { if (refId.Length > 2 && refId[1] == ':' && refId.IndexOfAny(new[] { '.', '(' }) != -1) { ns = refId.Trim(); // Strip off member name? if (!ns.StartsWith("R:", StringComparison.OrdinalIgnoreCase) && !ns.StartsWith("G:", StringComparison.OrdinalIgnoreCase) && !ns.StartsWith("N:", StringComparison.OrdinalIgnoreCase) && !ns.StartsWith("T:", StringComparison.OrdinalIgnoreCase)) { if (ns.IndexOf('(') != -1) { ns = ns.Substring(0, ns.IndexOf('(')); } if (ns.IndexOf('.') != -1) { ns = ns.Substring(0, ns.LastIndexOf('.')); } } if (ns.IndexOf('.') != -1) { ns = ns.Substring(2, ns.LastIndexOf('.') - 2); } else { ns = ns.Substring(2); } if (validNamespaces.Contains(ns) && !seenNamespaces.Contains(ns)) { seenNamespaces.Add(ns); yield return(ns); } } } } } }
/// <summary> /// Merge the conceptual topic IDs into the BuildAssembler manifest file. /// </summary> /// <param name="builder">The build process</param> private void MergeConceptualManifest(BuildProcess builder) { string conceptualManifest = Path.Combine(builder.WorkingFolder, "ConceptualManifest.xml"), referenceManifest = Path.Combine(builder.WorkingFolder, "manifest.xml"); builder.ReportProgress(" Merging topic IDs into manifest.xml"); using (var writer = XmlWriter.Create(conceptualManifest, new XmlWriterSettings { Indent = true, CloseOutput = true })) { writer.WriteStartDocument(); writer.WriteStartElement("topics"); foreach (TopicCollection tc in this.Topics) { foreach (Topic t in tc) { t.WriteManifest(writer, builder); } } if (File.Exists(referenceManifest)) { foreach (var topic in ComponentUtilities.XmlStreamAxis(referenceManifest, "topic")) { writer.WriteStartElement("topic"); foreach (var attr in topic.Attributes()) { writer.WriteAttributeString(attr.Name.LocalName, attr.Value); } writer.WriteEndElement(); } } writer.WriteEndElement(); writer.WriteEndDocument(); } if (File.Exists(referenceManifest)) { File.Copy(referenceManifest, Path.ChangeExtension(referenceManifest, "old"), true); } File.Copy(conceptualManifest, referenceManifest, true); File.Delete(conceptualManifest); }
/// <summary> /// This is used to get an enumerable list of unique namespaces referenced in the topic /// </summary> /// <param name="validNamespaces">An enumerable list of valid framework namespaces</param> /// <returns>An enumerable list of unique namespaces in the topic</returns> public IEnumerable <string> GetReferencedNamespaces(IEnumerable <string> validNamespaces) { HashSet <string> seenNamespaces = new HashSet <string>(); string ns; // Find all code entity references var entityRefs = ComponentUtilities.XmlStreamAxis(contentFile.FullPath, "codeEntityReference").Select(el => el.Value); foreach (var refName in entityRefs) { if (refName.Length > 2 && refName.IndexOfAny(new[] { '.', '(' }) != -1) { ns = refName.Trim(); // Strip off member name? if (!ns.StartsWith("R:", StringComparison.OrdinalIgnoreCase) && !ns.StartsWith("G:", StringComparison.OrdinalIgnoreCase) && !ns.StartsWith("N:", StringComparison.OrdinalIgnoreCase) && !ns.StartsWith("T:", StringComparison.OrdinalIgnoreCase)) { if (ns.IndexOf('(') != -1) { ns = ns.Substring(0, ns.IndexOf('(')); } if (ns.IndexOf('.') != -1) { ns = ns.Substring(0, ns.LastIndexOf('.')); } } if (ns.IndexOf('.') != -1) { ns = ns.Substring(2, ns.LastIndexOf('.') - 2); } else { ns = ns.Substring(2); } if (validNamespaces.Contains(ns) && !seenNamespaces.Contains(ns)) { seenNamespaces.Add(ns); yield return(ns); } } } }
/// <summary> /// This combines the conceptual and API intermediate TOC files into one file ready for transformation to /// the help format-specific TOC file formats and, if necessary, determines the default topic. /// </summary> private void CombineIntermediateTocFiles() { XmlAttribute attr; XmlDocument conceptualXml = null, tocXml; XmlElement docElement; XmlNodeList allNodes; XmlNode node, parent; int insertionPoint; this.ReportProgress(BuildStep.CombiningIntermediateTocFiles, "Combining conceptual and API intermediate TOC files..."); if (!this.ExecutePlugIns(ExecutionBehaviors.InsteadOf)) { this.ExecutePlugIns(ExecutionBehaviors.Before); // Load the TOC files if (toc != null && toc.Count != 0) { conceptualXml = new XmlDocument(); conceptualXml.Load(workingFolder + "_ConceptualTOC_.xml"); } tocXml = new XmlDocument(); tocXml.Load(workingFolder + "toc.xml"); // Merge the conceptual and API TOCs into one? if (conceptualXml != null) { // Remove the root content container if present as we don't need it for the other formats if ((project.HelpFileFormat & HelpFileFormats.MSHelpViewer) != 0 && !String.IsNullOrEmpty(this.RootContentContainerId)) { docElement = conceptualXml.DocumentElement; node = docElement.FirstChild; allNodes = node.SelectNodes("topic"); foreach (XmlNode n in allNodes) { n.ParentNode.RemoveChild(n); docElement.AppendChild(n); } node.ParentNode.RemoveChild(node); } if (String.IsNullOrEmpty(this.ApiTocParentId)) { // If not parented, the API content is placed above or below the conceptual content based // on the project's ContentPlacement setting. if (project.ContentPlacement == ContentPlacement.AboveNamespaces) { docElement = conceptualXml.DocumentElement; foreach (XmlNode n in tocXml.SelectNodes("topics/topic")) { node = conceptualXml.ImportNode(n, true); docElement.AppendChild(node); } tocXml = conceptualXml; } else { docElement = tocXml.DocumentElement; foreach (XmlNode n in conceptualXml.SelectNodes("topics/topic")) { node = tocXml.ImportNode(n, true); docElement.AppendChild(node); } } } else { // Parent the API content to a conceptual topic parent = conceptualXml.SelectSingleNode("//topic[@id='" + this.ApiTocParentId + "']"); // If not found, parent it to the root if (parent == null) { parent = conceptualXml.DocumentElement; } insertionPoint = this.ApiTocOrder; if (insertionPoint == -1 || insertionPoint >= parent.ChildNodes.Count) { insertionPoint = parent.ChildNodes.Count; } foreach (XmlNode n in tocXml.SelectNodes("topics/topic")) { node = conceptualXml.ImportNode(n, true); if (insertionPoint >= parent.ChildNodes.Count) { parent.AppendChild(node); } else { parent.InsertBefore(node, parent.ChildNodes[insertionPoint]); } insertionPoint++; } tocXml = conceptualXml; } // Fix up empty container nodes by removing the file attribute and setting the ID attribute to // the title attribute value. foreach (XmlNode n in tocXml.SelectNodes("//topic[@title]")) { attr = n.Attributes["file"]; if (attr != null) { n.Attributes.Remove(attr); } attr = n.Attributes["id"]; if (attr != null) { attr.Value = n.Attributes["title"].Value; } } tocXml.Save(workingFolder + "toc.xml"); } this.ExecutePlugIns(ExecutionBehaviors.After); } // Determine the default topic for Help 1, website, and markdown output if one was not specified in a // site map or content layout file. if (defaultTopic == null && (project.HelpFileFormat & (HelpFileFormats.HtmlHelp1 | HelpFileFormats.Website | HelpFileFormats.Markdown)) != 0) { var defTopic = ComponentUtilities.XmlStreamAxis(workingFolder + "toc.xml", "topic").FirstOrDefault( t => t.Attribute("file") != null); if (defTopic != null) { // Find the file. Could be .htm, .html, or .md so just look for any file with the given name. defaultTopic = Directory.EnumerateFiles(workingFolder + "Output", defTopic.Attribute("file").Value + ".*", SearchOption.AllDirectories).FirstOrDefault(); if (defaultTopic != null) { defaultTopic = defaultTopic.Substring(workingFolder.Length + 7); if (defaultTopic.IndexOf('\\') != -1) { defaultTopic = defaultTopic.Substring(defaultTopic.IndexOf('\\') + 1); } } } // This shouldn't happen anymore, but just in case... if (defaultTopic == null) { throw new BuilderException("BE0026", "Unable to determine default topic in toc.xml. Mark " + "one as the default topic manually in the content layout file."); } } }
//===================================================================== /// <summary> /// This is called to generate the namespace summary file /// </summary> private void GenerateNamespaceSummaries() { XmlNode member; NamespaceSummaryItem nsi; string nsName = null, summaryText; bool isDocumented; this.ReportProgress(BuildStep.GenerateNamespaceSummaries, "Generating namespace summary information..."); // Add a dummy file if there are no comments files specified. This will contain the project and // namespace summaries. if (commentsFiles.Count == 0) { nsName = workingFolder + "_ProjNS_.xml"; using (StreamWriter sw = new StreamWriter(nsName, false, Encoding.UTF8)) { sw.Write("<?xml version=\"1.0\"?>\r\n<doc>\r\n" + "<assembly>\r\n<name>_ProjNS_</name>\r\n" + "</assembly>\r\n<members/>\r\n</doc>\r\n"); } commentsFiles.Add(new XmlCommentsFile(nsName)); } // Replace any "NamespaceDoc" and "NamespaceGroupDoc" class IDs with their containing namespace. // The comments in these then become the comments for the namespaces and namespace groups. commentsFiles.ReplaceNamespaceDocEntries(); if (this.ExecutePlugIns(ExecutionBehaviors.InsteadOf)) { return; } this.ExecutePlugIns(ExecutionBehaviors.Before); // XML comments do not support summaries on namespace elements by default. However, if Sandcastle // finds them, it will add them to the help file. The same holds true for project comments on the // root namespaces page (R:Project_[HtmlHelpName]). We can accomplish this by adding elements to // the first comments file or by supplying them in an external XML comments file. try { // Add the project comments if specified if (project.ProjectSummary.Length != 0) { // Set the name in case it isn't valid nsName = "R:Project_" + this.ResolvedHtmlHelpName.Replace(" ", "_"); member = commentsFiles.FindMember(nsName); this.AddNamespaceComments(member, project.ProjectSummary); } // Get all the namespace and namespace group nodes from the reflection information file var nsElements = ComponentUtilities.XmlStreamAxis(reflectionFile, "api").Where(el => { string id = el.Attribute("id").Value; return(id.Length > 1 && id[1] == ':' && (id[0] == 'N' || id[0] == 'G') && el.Element("topicdata").Attribute("group").Value != "rootGroup"); }).Select(el => el.Attribute("id").Value); // Add the namespace summaries foreach (var n in nsElements) { nsName = n; nsi = project.NamespaceSummaries[nsName.StartsWith("N:", StringComparison.Ordinal) ? nsName.Substring(2) : nsName.Substring(2) + " (Group)"]; if (nsi != null) { isDocumented = nsi.IsDocumented; summaryText = nsi.Summary; } else { // The global namespace is not documented by default isDocumented = (nsName != "N:"); summaryText = String.Empty; } if (isDocumented) { // If documented, add the summary text member = commentsFiles.FindMember(nsName); this.AddNamespaceComments(member, summaryText); } } } catch (Exception ex) { // Eat the error in a partial build so that the user can get into the namespace comments editor // to fix it. if (this.PartialBuildType != PartialBuildType.None) { throw new BuilderException("BE0012", String.Format(CultureInfo.CurrentCulture, "Error generating namespace summaries (Namespace = {0}): {1}", nsName, ex.Message), ex); } } this.ExecutePlugIns(ExecutionBehaviors.After); }
/// <summary> /// This is used to get an enumerable list of unique namespaces referenced in the XML comments files /// </summary> /// <param name="validNamespaces">An enumerable list of valid namespaces</param> /// <returns>An enumerable list of unique namespaces referenced in the XML comments files</returns> public IEnumerable <string> GetReferencedNamespaces(IEnumerable <string> validNamespaces) { HashSet <string> seenNamespaces = new HashSet <string>(); string ns; foreach (XmlCommentsFile f in this) { // Find all comments elements with a reference. XML comments files may be ill-formed so // ignore any elements without a cref attribute. IEnumerable <string> crefs; try { crefs = ComponentUtilities.XmlStreamAxis(f.SourcePath, new[] { "event", "exception", "inheritdoc", "permission", "see", "seealso" }).Select( el => (string)el.Attribute("cref")).Where(c => c != null).ToList(); } catch (XmlException) { yield break; } foreach (string refId in crefs) { if (refId.Length > 2 && refId[1] == ':' && refId.IndexOfAny(new[] { '.', '(' }) != -1) { ns = refId.Trim(); // Strip off member name? if (!ns.StartsWith("R:", StringComparison.OrdinalIgnoreCase) && !ns.StartsWith("G:", StringComparison.OrdinalIgnoreCase) && !ns.StartsWith("N:", StringComparison.OrdinalIgnoreCase) && !ns.StartsWith("T:", StringComparison.OrdinalIgnoreCase)) { if (ns.IndexOf('(') != -1) { ns = ns.Substring(0, ns.IndexOf('(')); } if (ns.IndexOf('.') != -1) { ns = ns.Substring(0, ns.LastIndexOf('.')); } } if (ns.IndexOf('.') != -1) { ns = ns.Substring(2, ns.LastIndexOf('.') - 2); } else { ns = ns.Substring(2); } if (validNamespaces.Contains(ns) && !seenNamespaces.Contains(ns)) { seenNamespaces.Add(ns); yield return(ns); } } } } }