#region Copyright // -------------------------------------------------------------------------------------------------------------------- // // Copyright (C) 2015 Ian Horswill // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in the // Software without restriction, including without limitation the rights to use, copy, // modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, // and to permit persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // // -------------------------------------------------------------------------------------------------------------------- #endregion using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; namespace Prolog { /// /// Creates documentation for the Twig system /// [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly"), SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] public class Manual : IDisposable { private Manual(string path) { output = File.CreateText(path); } /// /// Release resources associated with output stream. /// [SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly"), SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")] public void Dispose() { output.Dispose(); } private readonly TextWriter output; private static readonly string[] ManualSections = { "h2|Flow control|Prolog", "h2|Meta-logical predicates|Prolog", "h2|All solutions predicates|Prolog", "h2|Arithmetic|Prolog", "h2|Comparisons|Prolog", "h2|List predicates|Prolog", "h2|Term manipulation|Prolog", "h2|Type predicates|Prolog", "h2|Constraint programming|Prolog", "h2|.NET interoperation|Prolog", "h2|Declarations|Prolog", "h2|Definite clause grammars|Prolog", "h2|Loading code|Prolog", "h2|Database manipulation|Prolog", "h2|eremic logic|Prolog", "h2|Other Predicates|Prolog" }; private static readonly Type[] ClassHierarchyRoots = { }; [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] static Manual() { foreach (var s in ManualSections) SectionTable[s.Split('|')[1].ToLower()] = new List(); } internal static Dictionary> SectionTable = new Dictionary>(); internal static void AddToSection(string name, Delegate d) { if (SectionTable.ContainsKey(name)) SectionTable[name].Add(d); else throw new ArgumentException("Unknown manual section: " + name); } internal static void AddToSections(string sections, Delegate d) { string[] sectionNames = sections.Split(','); foreach (var name in sectionNames) AddToSection(name, d); } /// /// Returns the text of the manual /// /// public override string ToString() { return output.ToString(); } private delegate void Thunk(); /// /// Output the manual entry for the specified procedure or macro. /// [SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "ignore"), SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "d")] public void WriteEntry(Delegate d, string language) { Tag("dt", () => Span("dullCode", delegate { if (language == "Prolog") { Span("procedureName", Htmlify(DelegateUtils.NamedProcedureTable[d])); var args = d.Arglist(); bool hasArg = false; #pragma warning disable 414, 168, 219 // ReSharper disable UnusedVariable foreach (var ignore in args) // ReSharper restore UnusedVariable #pragma warning restore 414, 168, 219 { hasArg = true; break; } if (hasArg) WriteArglist(d.Arglist(), true, true); } else { OpenParen(); Span("procedureName", Htmlify(DelegateUtils.NamedProcedureTable[d])); WriteArglist(d.Arglist(), false, false); } })); Tag("dd", Htmlify(d.Documentation())); } [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.String.ToLower"), SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] private void WriteLanguageSection(string sectionInfo, string language) { string[] info = sectionInfo.Split('|'); string sectionName = info[1]; string sectionType = info[0]; string lang = info[2]; if (lang == language) { List entries = SectionTable[sectionName.ToLower()]; //entries.Sort((d1, d2) => Writer.namedProcedureTable[d1].CompareTo(Writer.namedProcedureTable[d2])); Section(sectionType, sectionName); Tag("dl", delegate { foreach (var d in entries) WriteEntry(d, language); }); } } /// /// Build the actual manual text /// private void WriteBody(Thunk tocWriter, Thunk sectionWriter) { #if ClassDocumentation BuildSubclassTable(); #endif tocWriter(); sectionWriter(); //WriteLispTOC(); //WriteClassHierarchyTOC(); //WriteLispSections(); //WriteClassHierarchySections(); } private void WriteClassHierarchySections() { #if ClassDocumentation //output.Write(""); foreach (Type t in classHierarchyRoots) WriteClassHierarchy(t); DocumentRemainingClasses(); #endif } private void WriteAll(string title, Thunk tocWriter, Thunk sectionWriter) { output.Write(Header, title); Section("h1", title); WriteBody(tocWriter, sectionWriter); output.Write(Footer); } private void Close() { output.Close(); } /// /// Writes the Lisp manual to the specified path. /// private static void WriteLanguageManualToFile(string language, string title, string path) { using (var m = new Manual(path)) { // ReSharper disable AccessToDisposedClosure m.WriteAll(title, () => m.WriteLanguageTOC(language), () => m.WriteLanguageSections(language)); // ReSharper restore AccessToDisposedClosure m.Close(); } } /// /// Writes the Class manual to the specified path. /// [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] private static void WriteClassManualToFile(string path) { using (var m = new Manual(path)) { m.WriteAll("Twig Class Reference Manual", m.WriteClassHierarchyTOC, m.WriteClassHierarchySections); m.Close(); } } /// /// Writes all manual files to the specified directory /// /// [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "Twig.MinimaLisp.KnowledgeBase")] public static void WriteManual(string directory) { // ReSharper disable once ObjectCreationAsStatement new KnowledgeBase("foo", null); // Force InstallPrimitives() to run. WriteLanguageManualToFile("Prolog", "Unity Prolog Reference", directory + "\\Unity Prolog Reference.htm"); } [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] // ReSharper disable once InconsistentNaming private void WriteLanguageTOC(string language) { int currentLevel = 1; //Tag("h2", "Table of contents"); foreach (var sectionInfo in ManualSections) { string[] info = sectionInfo.Split('|'); string sectionName = info[1]; string sectionType = info[0]; string lang = info[2]; if (lang == language) { int sectionLevel = sectionType[1] - '0'; while (currentLevel != sectionLevel) { if (currentLevel < sectionLevel) { output.WriteLine("
    "); currentLevel++; } else { output.WriteLine("
"); currentLevel--; } } WriteTOCEntry(sectionName); } } while (currentLevel-- > 1) output.WriteLine(""); } // ReSharper disable once InconsistentNaming private void WriteClassHierarchyTOC() { //WriteTOCEntry("Class hierarchy"); Tag("ul", delegate { foreach (var t in ClassHierarchyRoots) WriteTOCEntry(t.Name + " hierarchy"); WriteTOCEntry("Other classes"); }); output.Write(""); } // ReSharper disable once InconsistentNaming private void WriteTOCEntry(string sectionName) { Tag("li", () => Tag("a", string.Format("href=\"#{0}\"", sectionName), () => output.Write(sectionName))); } [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] private void WriteLanguageSections(string language) { // ReSharper disable UnusedVariable foreach (var s in ManualSections) // ReSharper restore UnusedVariable { WriteLanguageSection(s, language); output.WriteLine(); } } //private readonly Dictionary> subclasses = new Dictionary>(); //private readonly Dictionary areClassMembersDocumented = new Dictionary(); //private readonly Dictionary documentationAlreadyPrinted = new Dictionary(); //[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] //private void AddSubclass(Type parent, Type subclass) //{ // if (parent == null) // return; // if (!subclasses.ContainsKey(parent)) // subclasses[parent] = new List(); // subclasses[parent].Add(subclass); //} #if ClassDocumentation private static readonly Assembly twigAssembly = Assembly.GetAssembly(typeof (PhysicalObject)); [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] private void BuildSubclassTable() { foreach (Type t in twigAssembly.GetTypes()) if (t.GetDocumentation() != null) { if (!t.IsAbstract) // Force generation of class documentation if it isn't abstract, since we at least want to show the constructors. areClassMembersDocumented[t] = true; else { foreach ( var member in t.GetMembers(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) { if (member.GetDocumentation() != null) { areClassMembersDocumented[t] = true; break; } } } AddSubclass(t.BaseType, t); } } private bool MembersAreDocumented(Type t) { bool v; if (areClassMembersDocumented.TryGetValue(t, out v)) return v; return false; } private static string ClassBookmark(Type t) { return "Class " + t.Name; } private string ClassReference(Type t) { if (MembersAreDocumented(t)) { return string.Format("{1}", ClassBookmark(t), TypeName(t)); } if (t.Namespace != null && t.Namespace.StartsWith("Microsoft.Xna")) return MSDNReference(t); return TypeName(t); } private static string MSDNReference(Type t) { System.Diagnostics.Debug.Assert(t.Namespace != null, "t.Namespace != null"); return string.Format("{2}", t.Namespace.ToLower(), t.Name.ToLower(), t.Name); } private string TypeName(Type t) { if (t == typeof (float)) return "float"; if (t == typeof (double)) return "double"; if (t == typeof (int)) return "int"; if (t == typeof (long)) return "long"; if (t == typeof (bool)) return "bool"; if (t == typeof (string)) return "string"; if (t == typeof (object)) return "object"; if (t == typeof (void)) return "void"; if (t.IsGenericType) { StringBuilder b = new StringBuilder(); string[] names = t.GetGenericTypeDefinition().Name.Split('`'); b.Append(names[0]); b.Append("<"); bool first = true; foreach (var a in t.GetGenericArguments()) { if (!first) b.Append(","); first = false; b.Append(TypeName(a)); } b.Append(">"); return b.ToString(); } return t.Name; } private void WriteSubhierarchy(Type t) { Tag("li", delegate { Span("dullCode", ClassReference(t)); string documentation = t.GetDocumentation(); if (documentation != null) { output.Write("
"); output.Write(documentation); } }); if (subclasses.ContainsKey(t)) { Tag("ul", delegate { foreach (Type c in subclasses[t]) WriteSubhierarchy(c); }); } } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] private void WriteClassHierarchy(Type t) { Section("h1", t.Name + " hierarchy", t.Name + " hierarchy"); Tag("ul", () => WriteSubhierarchy(t)); DocumentSubhierarchy(t); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] private void DocumentSubhierarchy(Type t) { if (MembersAreDocumented(t)) DocumentClass(t); if (subclasses.ContainsKey(t)) { foreach (Type c in subclasses[t]) DocumentSubhierarchy(c); } } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity") ] private void DocumentClass(Type t) { if (documentationAlreadyPrinted.ContainsKey(t)) return; documentationAlreadyPrinted[t] = true; Section("h2", t.Name + " class", ClassBookmark(t)); Span("argument", delegate { if (t.IsEnum) { output.Write("Enumerated type"); } else if (t.IsValueType) { output.Write("Value type"); } else { output.Write("Superclasses: "); Type p = t.BaseType; output.Write(ClassReference(p)); System.Diagnostics.Debug.Assert(p != null, "p != null"); p = p.BaseType; while (p != null && p != typeof (object)) { output.Write(", "); output.Write(ClassReference(p)); p = p.BaseType; } List subs; if (subclasses.TryGetValue(t, out subs) && subs.Count > 0) { output.Write("
Subclasses: "); for (int i = 0; i < subs.Count; i++) { if (i > 0) output.Write(", "); output.Write(ClassReference(subs[i])); } } } }); string doc = t.GetDocumentation(); if (doc != null) Tag("p", doc); if (t.IsSubclassOf(typeof (PhysicalObject))) { DocumentMembers( t.GetConstructors(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static), "Constructors", !t.IsAbstract, delegate(ConstructorInfo c) { ParameterInfo[] p = c.GetParameters(); if (p.Length >= 2 && p[1].ParameterType == typeof (string)) { Span("dullCode", "(define-twig-object "); Span("argument", "name"); Span("dullCode", " " + t.Name + ((p.Length > 2) ? " " : "")); Span("argument", delegate { for (int i = 2; i < p.Length; i++) { output.Write("   "); output.Write(p[i].Name); output.Write(":"); output.Write(ClassReference(p[i].ParameterType)); } }); } else { Span("dullCode", "(new-component "); Span("dullCode", t.Name + ((p.Length > 1) ? " " : "")); Span("argument", delegate { for (int i = 1; i < p.Length; i++) { output.Write("   "); output.Write(p[i].Name); output.Write(":"); output.Write(ClassReference(p[i].ParameterType)); } }); } Span("dullCode", ")"); }); } else if (t.IsSubclassOf(typeof (TwigGameComponent))) { DocumentMembers( t.GetConstructors(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static), "Constructors", !t.IsAbstract, delegate(ConstructorInfo c) { ParameterInfo[] p = c.GetParameters(); Span("dullCode", "(new-component " + t.Name + ((p.Length > 1) ? " " : "")); Span("argument", delegate { for (int i = 1; i < p.Length; i++) { output.Write("   "); output.Write(p[i].Name); output.Write(":"); output.Write(ClassReference(p[i].ParameterType)); } }); Span("dullCode", ")"); }); } else if (!t.IsEnum) { DocumentMembers( t.GetConstructors(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static), "Constructors", !t.IsAbstract, delegate(ConstructorInfo c) { Span("dullCode", delegate { output.Write("(new "); output.Write(t.Name); }); Span("argument", delegate { ParameterInfo[] p = c.GetParameters(); foreach (ParameterInfo t1 in p) { output.Write("   "); output.Write(t1.Name); output.Write(":"); output.Write(ClassReference(t1.ParameterType)); } }); Span("dullCode", ")"); }); } DocumentMembers( t.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static), t.IsEnum ? "Values" : "Fields", t.IsEnum, delegate(FieldInfo f) { if (f.IsStatic) Span("dullCode", t.Name + "." + f.Name); else Span("dullCode", f.Name); if (!t.IsEnum) Span("argument", " : " + ClassReference(f.FieldType)); }); DocumentMembers( t.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static), "Properties", t.IsEnum, delegate(PropertyInfo p) { if (p.GetAccessors()[0].IsStatic) Span("dullCode", t.Name + "." + p.Name); else Span("dullCode", p.Name); Span("argument", " : " + ClassReference(p.PropertyType)); }); DocumentMembers( t.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static), "Methods", false, delegate(MethodInfo m) { Span("dullCode", "("); if (m.IsStatic) Span("dullCode", t.Name); else Span("argument", t.Name.ToLower()); Span("dullCode", "." + m.Name); Span("argument", delegate { ParameterInfo[] p = m.GetParameters(); foreach (ParameterInfo t1 in p) { output.Write("   "); output.Write(t1.Name); output.Write(":"); output.Write(ClassReference(t1.ParameterType)); } }); Span("dullCode", ")"); output.Write(" : "); Span("argument", ClassReference(m.ReturnType)); }); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] private void DocumentRemainingClasses() { List remainingClasses = new List(); foreach (var pair in areClassMembersDocumented) if (!documentationAlreadyPrinted.ContainsKey(pair.Key)) remainingClasses.Add(pair.Key); // ReSharper disable StringCompareIsCultureSpecific.1 remainingClasses.Sort((t1, t2) => string.Compare(t1.Name, t2.Name)); // ReSharper restore StringCompareIsCultureSpecific.1 Section("h1", "Other classes"); foreach (var c in remainingClasses) DocumentClass(c); } private delegate void MemberFormatter(T member); [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1307:SpecifyStringComparison", MessageId = "System.String.Compare(System.String,System.String)")] private void DocumentMembers(T[] members, string heading, bool forceDocumentation, MemberFormatter format) where T : MemberInfo { bool foundOne = false; foreach (var m in members) if (forceDocumentation || m.GetDocumentation() != null) { foundOne = true; break; } if (foundOne) { Section("h3", heading, null); List mems = new List(members); // ReSharper disable StringCompareIsCultureSpecific.1 mems.Sort((m1, m2) => (string.Compare(m1.Name, m2.Name))); // ReSharper restore StringCompareIsCultureSpecific.1 Tag("dl", delegate { foreach (var m in mems) { string doc = m.GetDocumentation(); if (m.Name != "value__" && (forceDocumentation || doc != null)) { // ReSharper disable AccessToModifiedClosure Tag("dt", () => format(m)); // ReSharper restore AccessToModifiedClosure if (doc != null) Tag("dd", doc); } } }); } } #endif [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] private void Section(string sectionType, string sectionName) { Section(sectionType, sectionName, sectionName); } [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] private void Section(string sectionType, string sectionName, string bookmarkName) { if (bookmarkName != null) Tag(sectionType, () => Tag("a", string.Format("name=\"{0}\"", bookmarkName), sectionName)); else Tag(sectionType, sectionName); } private void Span(string style, Thunk p) { Tag("span", string.Format("class=\"{0}\"", style), p); } private void Span(string style, string s) { Tag("span", string.Format("class=\"{0}\"", style), () => output.Write(s)); } private void Tag(string name, Thunk p) { output.Write("<{0}>", name); p(); output.Write("", name); } private void Tag(string name, string content) { Tag(name, () => output.Write(content)); } private void Tag(string name, string args, string content) { Tag(name, args, () => output.Write(content)); } private void Tag(string name, string args, Thunk p) { output.Write("<{0} {1}>", name, args); p(); output.Write("", name); } private void Space(bool needComma) { output.Write(needComma ? ", " : " "); } private void WriteArglist(IEnumerable args, bool printOpenParen, bool printComma) { bool needSpace = !printOpenParen; if (printOpenParen) OpenParen(); foreach (object arg in args) { if (needSpace) Space(printComma); needSpace = true; var s = arg as Symbol; var str = arg as string; if (s != null) str = s.Name; var sublist = arg as IEnumerable; if (str != null) Span("argument", Htmlify(str)); else if (sublist != null) WriteArglist(sublist, true, false); else throw new ArgumentException("Improper data type in argument list"); } CloseParen(); } private static string Htmlify(string str) { if (str == "...") return "…"; return str.Replace(">", ">").Replace("<", "<"); } private void OpenParen() { //Span("dullCode", "("); output.Write("("); } private void CloseParen() { //Span("dullCode", ")"); output.Write(")"); } #region Header and footer private const string Header = "\n\n\n\n\n{0}\n\n\n\n"; private const string Footer = "\n\n"; #endregion #region Emacs autocomplete file generator /// /// Writes the Emacs AutoCompleteMode config files /// public static void WriteEmacsAutoCompleteFile() { using (var m = new Manual("documentation.el")) { m.WriteAutoComplete("Lisp"); m.WriteAutoComplete("Prolog"); } } void WriteAutoComplete(string language) { string tableName = "twig-" + language.ToLower() + "-auto-complete-table"; output.WriteLine("(defvar {0} (make-hash-table :test 'equal))", tableName); foreach (var sections in ManualSections) { string[] info = sections.Split('|'); string sectionName = info[1]; string lang = info[2]; if (lang == language) { foreach (Delegate proc in SectionTable[sectionName.ToLower()]) { string procedureName = DelegateUtils.NamedProcedureTable[proc]; output.WriteLine( "(puthash \"{0}\" '((procedure . \"{1}\") (arguments . ({2})) (docstring . \"{3}\")) {4})", procedureName, procedureName, FormatArgumentsForEmacs(proc.Arglist()), proc.Documentation(), tableName); } } } } private string FormatArgumentsForEmacs(IEnumerable arglist) { var s = new StringBuilder(); foreach (var arg in arglist) { s.AppendFormat("\"{0}\" ", arg); } return s.ToString(); } #endregion } }