diff --git a/SHFB/Source/PresentationStyles/Default2022/Default2022Transformation.cs b/SHFB/Source/PresentationStyles/Default2022/Default2022Transformation.cs index d4cc09b7..9a998d4b 100644 --- a/SHFB/Source/PresentationStyles/Default2022/Default2022Transformation.cs +++ b/SHFB/Source/PresentationStyles/Default2022/Default2022Transformation.cs @@ -1,4 +1,4 @@ -//=============================================================================================================== +//=============================================================================================================== // System : Sandcastle Tools Standard Presentation Styles // File : Default2022Transformation.cs // Author : Eric Woodruff (Eric@EWoodruff.us) @@ -1143,9 +1143,18 @@ private void RenderPageTitleAndLogo(XElement body) private static void RenderNotices(TopicTransformationCore transformation) { var preliminary = transformation.CommentsNode.Element("preliminary"); - var obsolete = transformation.ReferenceNode.AttributeOfType("T:System.ObsoleteAttribute"); + + List attributeRepresentations = new List(); + + foreach(var keyValuePair in ((Default2022Transformation)transformation).presentationStyle.LongAttributeRepresentations) + { + if(transformation.ReferenceNode.AttributeOfType(keyValuePair.Key) != null) + { + attributeRepresentations.Add(keyValuePair.Value); + } + } - if(preliminary != null || obsolete != null) + if(preliminary != null || attributeRepresentations.Count > 0) { var currentElement = transformation.CurrentElement; var notes = new XElement("span", new XAttribute("class", "tags")); @@ -1157,13 +1166,11 @@ private static void RenderNotices(TopicTransformationCore transformation) if(preliminary != null) transformation.RenderNode(preliminary); - if(obsolete != null) + foreach(XNode attributeRepresentation in attributeRepresentations) { - notes.Add(new XElement("span", - new XAttribute("class", "tag is-danger is-medium"), - new XElement("include", new XAttribute("item", "boilerplate_obsoleteLong")))); + notes.Add(attributeRepresentation); } - + transformation.CurrentElement = currentElement; } } @@ -1604,20 +1611,26 @@ private static void RenderApiNamespaceList(TopicTransformationCore transformatio if(summary != null) transformation.RenderChildElements(summaryCell, summary.Nodes()); - var obsoleteAttr = e.AttributeOfType("T:System.ObsoleteAttribute"); + List attributeRepresentations = new List(); + + foreach(var keyValuePair in ((Default2022Transformation)transformation).presentationStyle.ShortAttributeRepresentations) + { + if(e.AttributeOfType(keyValuePair.Key) != null) + { + attributeRepresentations.Add(keyValuePair.Value); + } + } + var prelimComment = e.Element("preliminary"); - if(obsoleteAttr != null || prelimComment != null) + if(attributeRepresentations.Count > 0 || prelimComment != null) { if(!summaryCell.IsEmpty) summaryCell.Add(new XElement("br")); - if(obsoleteAttr != null) + foreach(XNode attributeRepresentation in attributeRepresentations) { - summaryCell.Add(new XElement("span", - new XAttribute("class", "tag is-danger"), - new XElement("include", - new XAttribute("item", "boilerplate_obsoleteShort")))); + summaryCell.Add(attributeRepresentation); } if(prelimComment != null) @@ -1779,15 +1792,25 @@ private static void RenderApiEnumerationMembersList(TopicTransformationCore tran thisTransform.RenderChildElements(summaryCell, remarks.Nodes()); } - if(e.AttributeOfType("T:System.ObsoleteAttribute") != null) + List attributeRepresentations = new List(); + + foreach(var keyValuePair in ((Default2022Transformation)transformation).presentationStyle.ShortAttributeRepresentations) + { + if(e.AttributeOfType(keyValuePair.Key) != null) + { + attributeRepresentations.Add(keyValuePair.Value); + } + } + + if(attributeRepresentations.Count > 0) { if(!summaryCell.IsEmpty) summaryCell.Add(new XElement("br")); - summaryCell.Add(new XElement("span", - new XAttribute("class", "tag is-danger"), - new XElement("include", - new XAttribute("item", "boilerplate_obsoleteShort")))); + foreach(XNode attributeRepresentation in attributeRepresentations) + { + summaryCell.Add(attributeRepresentation); + } } if(summaryCell.IsEmpty) @@ -2036,20 +2059,25 @@ private static void RenderApiTypeMemberLists(TopicTransformationCore transformat } } - var obsoleteAttr = e.AttributeOfType("T:System.ObsoleteAttribute"); + List attributeRepresentations = new List(); + + foreach(var keyValuePair in ((Default2022Transformation)transformation).presentationStyle.ShortAttributeRepresentations) + { + if(e.AttributeOfType(keyValuePair.Key) != null) + { + attributeRepresentations.Add(keyValuePair.Value); + } + } var prelimComment = e.Element("preliminary"); - if(obsoleteAttr != null || prelimComment != null) + if(attributeRepresentations.Count > 0 || prelimComment != null) { if(!summaryCell.IsEmpty) summaryCell.Add(new XElement("br")); - if(obsoleteAttr != null) + foreach(XNode attributeRepresentation in attributeRepresentations) { - summaryCell.Add(new XElement("span", - new XAttribute("class", "tag is-danger"), - new XElement("include", - new XAttribute("item", "boilerplate_obsoleteShort")))); + summaryCell.Add(attributeRepresentation); } if(prelimComment != null) diff --git a/SHFB/Source/SandcastleBuilderPlugIns/AttributeRendererPlugin.cs b/SHFB/Source/SandcastleBuilderPlugIns/AttributeRendererPlugin.cs new file mode 100644 index 00000000..bfa90fe8 --- /dev/null +++ b/SHFB/Source/SandcastleBuilderPlugIns/AttributeRendererPlugin.cs @@ -0,0 +1,140 @@ +//=============================================================================================================== +// System : Sandcastle Help File Builder Plug-Ins +// File : XPathReflectionFileFilterPlugIn.cs +// Author : Eric Woodruff (Eric@EWoodruff.us) Based on code by Eyal Post +// Updated : 06/17/2021 +// Note : Copyright 2008-2021, Eric Woodruff, All rights reserved +// +// This file contains a plug-in that is used to filter out unwanted information from the reflection information +// file using XPath queries. +// +// This code is published under the Microsoft Public License (Ms-PL). A copy of the license should be +// distributed with the code and can be found at the project website: https://GitHub.com/EWSoftware/SHFB. This +// notice, the author's name, and all copyright notices must remain intact in all applications, documentation, +// and source files. +// +// Date Who Comments +// ============================================================================================================== +// 10/31/2008 EFW Created the code +// 12/17/2013 EFW Updated to use MEF for the plug-ins +//=============================================================================================================== + +// Ignore Spelling: Eyal + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Xml; +using System.Xml.Linq; + +using SandcastleBuilder.Utils; +using SandcastleBuilder.Utils.BuildComponent; +using SandcastleBuilder.Utils.BuildEngine; + +namespace SandcastleBuilder.PlugIns +{ + /// + /// This plug-in class is used to define how attributes are rendered + /// + [HelpFileBuilderPlugInExport("Attribute Renderer Plugin", RunsInPartialBuild = true, + Version = AssemblyInfo.ProductVersion, Copyright = AssemblyInfo.Copyright + "\r\nBased on code submitted by " + + "Eyal Post", Description = "This plug-in is used to define how attributes such as 'Obsolete' are rendered.")] + public sealed class AttributeRendererPlugin : IPlugIn + { + #region Private data members + //===================================================================== + + private List executionPoints; + private BuildProcess builder; + private readonly ObservableCollection attributeRepresentationEntries = new ObservableCollection(); + + #endregion + + #region IPlugIn implementation + //===================================================================== + + /// + /// This read-only property returns a collection of execution points that define when the plug-in should + /// be invoked during the build process. + /// + public IEnumerable ExecutionPoints + { + get + { + if(executionPoints == null) + executionPoints = new List + { + new ExecutionPoint(BuildStep.ApplyDocumentModel, ExecutionBehaviors.Before) + }; + + return executionPoints; + } + } + + /// + /// This method is used to initialize the plug-in at the start of the build process + /// + /// A reference to the current build process + /// The configuration data that the plug-in should use to initialize itself + public void Initialize(BuildProcess buildProcess, XElement configuration) + { + if(configuration == null) + throw new ArgumentNullException(nameof(configuration)); + + builder = buildProcess; + + var metadata = (HelpFileBuilderPlugInExportAttribute)this.GetType().GetCustomAttributes( + typeof(HelpFileBuilderPlugInExportAttribute), false).First(); + + builder.ReportProgress("{0} Version {1}\r\n{2}", metadata.Id, metadata.Version, metadata.Copyright); + + this.ReadConfiguration(configuration); + + if(attributeRepresentationEntries.Count == 0) + { + throw new BuilderException("ATTR0001", "No AttributeRepresentations defined " + + "Attribute Renderer plug-in"); + } + } + + public void ReadConfiguration(XElement configuration) + { + foreach(var expr in configuration.Descendants("AttributeRepresentationEntry")) + AttributeRepresentationEntries.Add(AttributeRepresentationEntry.FromXml(this, expr)); + } + + /// + /// This method is used to execute the plug-in during the build process + /// + /// The current execution context + public void Execute(ExecutionContext context) + { + foreach(var item in attributeRepresentationEntries) + { + if (item.HasLongRepresentation) builder.PresentationStyle.LongAttributeRepresentations[item.AttributeClassName] = item.GetLongRepresentation(); + if (item.HasShortRepresentation) builder.PresentationStyle.ShortAttributeRepresentations[item.AttributeClassName] = item.GetShortRepresentation(); + } + } + #endregion + + #region Properties + + public ObservableCollection AttributeRepresentationEntries => attributeRepresentationEntries; + + #endregion + + #region IDisposable implementation + //===================================================================== + + /// + /// This implements the Dispose() interface to properly dispose of the plug-in object + /// + public void Dispose() + { + // Nothing to dispose of in this one + GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/SHFB/Source/SandcastleBuilderPlugIns/AttributeRepresentationEntry.cs b/SHFB/Source/SandcastleBuilderPlugIns/AttributeRepresentationEntry.cs new file mode 100644 index 00000000..02f6ae5b --- /dev/null +++ b/SHFB/Source/SandcastleBuilderPlugIns/AttributeRepresentationEntry.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using SandcastleBuilder.Utils; + +namespace SandcastleBuilder.PlugIns +{ + public class AttributeRepresentationEntry : INotifyPropertyChanged + { + public AttributeRepresentationEntry(AttributeRendererPlugin parent) + { + Parent = parent; + } + + private AttributeRendererPlugin Parent { get; } + + private string attributeClassName; + + /// + /// This is the full name of the Attribute to render, for example: T:System.ObsoleteAttribute + /// + public string AttributeClassName + { + get => attributeClassName; + set + { + if(attributeClassName != value) + { + attributeClassName = value?.Trim(); + + this.Validate(); + this.OnPropertyChanged(); + } + } + } + + private string sortRepresentation; + + /// + /// This is the full name of the Attribute to render, for example: T:System.ObsoleteAttribute + /// + public string ShortRepresentation + { + get => sortRepresentation; + set + { + if(sortRepresentation != value) + { + sortRepresentation = value?.Trim(); + + this.Validate(); + this.OnPropertyChanged(); + } + } + } + + private string longRepresentation; + + /// + /// This is the full name of the Attribute to render, for example: T:System.ObsoleteAttribute + /// + public string LongRepresentation + { + get => longRepresentation; + set + { + if(longRepresentation != value) + { + longRepresentation = value?.Trim(); + + this.Validate(); + this.OnPropertyChanged(); + } + } + } + + #region INotifyPropertyChanged implementation + + //===================================================================== + + /// + /// The property changed event + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// This raises the event + /// + /// The property name that changed + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + + #region Helper methods + + //===================================================================== + + private string errorMessage; + + /// + /// This read-only property returns an error message describing any issues with the settings + /// + public string ErrorMessage + { + get => errorMessage; + private set + { + errorMessage = value; + + this.OnPropertyChanged(); + } + } + + public bool HasLongRepresentation => !String.IsNullOrWhiteSpace(LongRepresentation); + public bool HasShortRepresentation => !String.IsNullOrWhiteSpace(ShortRepresentation); + + + /// + /// This is used to validate the settings + /// + private void Validate() + { + List problems = new List(); + + if(String.IsNullOrWhiteSpace(AttributeClassName)) + problems.Add("An attribute name is required"); + + if(!HasShortRepresentation && !HasLongRepresentation) + problems.Add("A representation is required"); + + if (Parent.AttributeRepresentationEntries.Count(x => x!= this && x.AttributeClassName.Equals(attributeClassName, StringComparison.InvariantCulture)) > 1) + problems.Add($"Representation for {attributeClassName} registered twice."); + + if(problems.Count != 0) + this.ErrorMessage = String.Join(" / ", problems); + else + this.ErrorMessage = null; + } + + #endregion + + #region Convert from/to XML + + //===================================================================== + + /// + /// Create a binding redirect settings instance from an XML element containing the settings + /// + /// The base path provider object + /// The XML element from which to obtain the settings + /// A object containing the settings from the XPath + /// navigator. + /// It should contain an element called dependentAssembly with a configFile + /// attribute or a nested assemblyIdentity and bindingRedirect element that define + /// the settings. + public static AttributeRepresentationEntry FromXml(AttributeRendererPlugin parent, XElement configuration) + { + AttributeRepresentationEntry entry = new AttributeRepresentationEntry(parent); + + if(configuration != null) + { + entry.AttributeClassName = configuration.Element("AttributeClassName")?.Value; + entry.ShortRepresentation = configuration.Element("ShortRepresentation")?.Value; + entry.LongRepresentation = configuration.Element("LongRepresentation")?.Value; + } + + return entry; + } + + /// + /// Store the binding redirect settings in an XML element + /// + /// True to allow a relative path on importFrom attributes, false to + /// fully qualify the path. + /// Returns the XML element + /// The settings are stored in an element called dependentAssembly. + public XElement ToXml() + { + var el = new XElement("AttributeRepresentationEntry", + new XElement("AttributeClassName", AttributeClassName), + new XElement("ShortRepresentation", ShortRepresentation), + new XElement("LongRepresentation", LongRepresentation) + ); + + return el; + } + + #endregion + + public XElement GetLongRepresentation() + { + if (String.IsNullOrWhiteSpace(LongRepresentation)) return null; + + // try to parse XElement + var el = TryParseXElement(LongRepresentation); + + if(el == null || el.IsEmpty) + { + object content = GetContent(LongRepresentation); + string classes = GetStyleClasses(LongRepresentation, "tag is-danger is-medium"); + + return new XElement("span", + new XAttribute("class", classes), + content); + } + else + { + return el; + } + } + + public XElement GetShortRepresentation() + { + if (String.IsNullOrWhiteSpace(ShortRepresentation)) return null; + + // try to parse XElement + var el = TryParseXElement(ShortRepresentation); + + if(el == null || el.IsEmpty) + { + object content = GetContent(ShortRepresentation); + string classes = GetStyleClasses(ShortRepresentation, "tag is-danger"); + + return new XElement("span", + new XAttribute("class", classes), + content); + } + else + { + return el; + } + } + + /// + /// Tries to parse a to . + /// + /// The content to parse + /// The if successful, otherwise + private static XElement TryParseXElement(string content) + { + if (String.IsNullOrWhiteSpace(content)) return null; + + try + { + return XElement.Parse(content); + } + catch + { + return null; + } + } + + private static string GetStyleClasses(string representation, string fallbackClasses) + { + if (String.IsNullOrWhiteSpace(representation)) return fallbackClasses; + + // tags can be added by [[mytag]]. Search for them + var matches = Regex.Matches(representation, @"(?<=\[\[).*?(?=\]\])"); + + var classes = matches.Cast().Select(m => m.Value); + + return matches.Count == 0 ? fallbackClasses : String.Join(" ", classes); + } + + private static XNode GetContent(string representation) + { + // tags can be added by [[mytag]] so we need to remove this string carefully. + representation = Regex.Replace(representation, @"(?<=\[\[).*?(?=\]\])", ""); + + XNode content = representation.StartsWith("@") + ? new XElement("include", new XAttribute("item", representation.Substring(1).Trim())) as XNode + : new XText(representation); + return content; + } + } +} \ No newline at end of file diff --git a/SHFB/Source/SandcastleBuilderPlugInsUI/AttributeRendererConfigDlg.xaml b/SHFB/Source/SandcastleBuilderPlugInsUI/AttributeRendererConfigDlg.xaml new file mode 100644 index 00000000..ec964c1e --- /dev/null +++ b/SHFB/Source/SandcastleBuilderPlugInsUI/AttributeRendererConfigDlg.xaml @@ -0,0 +1,120 @@ + + + + + + + + + + +