private void ParseFile(string filename, Version apiVersion) { var document = ApiNode.Load(filename); var section = document?.SelectOne(@"//div.content/div.section"); var header = section?.SelectOne(@"div.mb20.clear"); var name = header?.SelectOne(@"h1.heading.inherit"); // Type or type member name var ns = header?.SelectOne(@"p"); // "class in {ns}" // Only interested in types at this point if (name == null || ns == null) { return; } // Only types that have messages var messages = section.Subsection("Messages").ToArray(); if (messages.Length == 0) { return; } var match = NsRegex.Match(ns.Text); var clsType = match.Groups["type"].Value; var nsName = match.Groups["namespace"].Value; var unityApiType = api.AddType(nsName, name.Text, clsType, filename, apiVersion); foreach (var message in messages) { var eventFunction = ParseMessage(message, apiVersion, nsName); unityApiType.MergeEventFunction(eventFunction, apiVersion); } }
private UnityApiEventFunction ParseMessage(ApiNode message, Version apiVersion, string hintNamespace) { var link = message.SelectOne(@"td.lbl/a"); var desc = message.SelectOne(@"td.desc"); if (link == null || desc == null) { return(null); } var detailsPath = link[@"href"]; if (string.IsNullOrWhiteSpace(detailsPath)) { return(null); } var path = Path.Combine(myScriptReferenceRelativePath, detailsPath); if (!File.Exists(path)) { return(null); } var detailsDoc = ApiNode.Load(path); var details = detailsDoc?.SelectOne(@"//div.content/div.section"); var signature = details?.SelectOne(@"div.mb20.clear/h1.heading.inherit"); var staticNode = details?.SelectOne(@"div.subsection/p/code.varname[text()='static']"); if (signature == null) { return(null); } var isCoroutine = CoroutineRegex.IsMatch(details.Text); var messageName = link.Text; var returnType = ApiType.Void; string[] argumentNames = null; var isStaticFromExample = false; var example = PickExample(details); if (example != null) { var tuple = ParseDetailsFromExample(messageName, example, hintNamespace); returnType = tuple.Item1; argumentNames = tuple.Item2; isStaticFromExample = tuple.Item3; } var docPath = Path.Combine(myScriptReferenceRelativePath, detailsPath); var eventFunction = new UnityApiEventFunction(messageName, staticNode != null || isStaticFromExample, isCoroutine, returnType, apiVersion, desc.Text, docPath); ParseParameters(eventFunction, signature, details, hintNamespace, argumentNames); return(eventFunction); }
private void ParseFile([NotNull] string filename) { var document = ApiNode.Load(filename); var section = document?.SelectOne(@"//div.content/div.section"); var header = section?.SelectOne(@"div.mb20.clear"); var cls = header?.SelectOne(@"h1.heading.inherit"); var ns = header?.SelectOne(@"p"); if (cls == null || ns == null) { return; } var messages = section.Subsection("Messages").ToArray(); if (messages.Length == 0) { return; } api.Enter("type"); var clsType = NsRegex.Replace(ns.Text, "$1"); api.SetAttribute("kind", clsType); api.SetAttribute("name", cls.Text); var nsName = NsRegex.Replace(ns.Text, "$2"); api.SetAttribute(@"ns", nsName); var hostType = new ApiType(string.Concat(nsName, ".", cls.Text)); api.SetAttribute("path", new Uri(filename).AbsoluteUri); foreach (var message in messages) { string detailsPath; ApiType type; if (!ParseMessage(message, out detailsPath, out type)) { continue; } api.LeaveTo("message"); api.SetAttribute("path", new Uri(detailsPath).AbsoluteUri); api.Enter("returns"); api.SetAttribute("type", type.FullName); api.SetAttribute("array", type.IsArray); } }
private static ApiNode[] PickExample([NotNull] ApiNode details) { // Favour C#, it's the most strongly typed var examples = PickExample(details, "CS"); if (examples.IsEmpty()) { examples = PickExample(details, "JS"); } if (examples.IsEmpty()) { examples = PickExample(details, "Raw"); } return(examples); }
private static void ParseMessageExample(string messageName, IReadOnlyList <Argument> arguments, ApiNode example, ref ApiType type) { var blankCleanup1 = new Regex(@"\s+"); var blankCleanup2 = new Regex(@"\s*(\W)\s*"); var exampleText = example.Text; exampleText = blankCleanup1.Replace(exampleText, " "); exampleText = blankCleanup2.Replace(exampleText, "$1"); var jsRegex = new Regex($@"(?:\W|^)function {messageName}\(([^)]*)\)(?::(\w+))?\{{"); var m = jsRegex.Match(exampleText); if (m.Success) { type = new ApiType(m.Groups[2].Value); var parameters = m.Groups[1].Value.Split(','); for (var i = 0; i < arguments.Count; ++i) { arguments[i].Name = parameters[i].Split(':')[0]; } return; } var csRegex = new Regex($@"(\w+) {messageName}\(([^)]*)\)"); m = csRegex.Match(exampleText); if (m.Success) { var nameRegex = new Regex(@"\W(\w+)$"); type = new ApiType(m.Groups[1].Value); var parameters = m.Groups[2].Value.Split(','); for (var i = 0; i < arguments.Count; ++i) { arguments[i].Name = nameRegex.Replace(parameters[i], "$1"); } return; } Console.WriteLine(exampleText); }
private static void ResolveArguments([NotNull] ApiNode details, [NotNull] IReadOnlyList <Argument> arguments, string[] argumentNames) { for (var i = 0; i < arguments.Count && i < argumentNames.Length; i++) { if (!string.IsNullOrEmpty(argumentNames[i])) { arguments[i].Name = argumentNames[i]; } } var parameters = details.Subsection("Parameters").ToArray(); if (Enumerable.Any(parameters)) { ParseMessageParameters(arguments, parameters); } }
private static void ResolveArguments([NotNull] string message, [NotNull] ApiNode details, [NotNull] IReadOnlyList <Argument> arguments, [NotNull] ref ApiType type) { var parameters = details.Subsection("Parameters").ToArray(); if (parameters.Any()) { ParseMessageParameters(arguments, parameters); return; } var example = PickExample(details); if (example == null) { return; } ParseMessageExample(message, arguments, example, ref type); }
private static void ParseParameters(UnityApiEventFunction eventFunction, ApiNode signature, ApiNode details, string owningMessageNamespace, string[] argumentNames) { // Capture the arguments string. Note that this might be `string s, int i` or `string, int` var match = CaptureArgumentsRegex.Match(signature.Text); if (!match.Success) { return; } var argumentString = match.Groups["args"].Value; var argumentStrings = argumentString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) .ToArray(); var total = argumentStrings.Length; var arguments = argumentStrings.Select((s, i) => new Argument(s, i, total, owningMessageNamespace)).ToArray(); ResolveArguments(details, arguments, argumentNames); foreach (var argument in arguments) { eventFunction.AddParameter(argument.Name, argument.Type, argument.Description); } }
private static void ParseParameters(UnityApiEventFunction eventFunction, ApiNode signature, ApiNode details, string owningMessageNamespace, string[] argumentNames) { // E.g. OnCollisionExit2D(Collision2D) - doesn't always include the argument name // Hopefully, we parsed the argument name from the example var argumentString = SigRegex.Replace(signature.Text, "$2$3"); if (string.IsNullOrWhiteSpace(argumentString)) { return; } var argumentStrings = argumentString.Split(',') .Select(s => s.Trim()) .ToArray(); var total = argumentStrings.Length; var arguments = argumentStrings.Select((s, i) => new Argument(s, i, total, owningMessageNamespace)).ToArray(); ResolveArguments(details, arguments, argumentNames); foreach (var argument in arguments) { eventFunction.AddParameter(argument.Name, argument.Type, argument.Description); } }
private static ApiNode PickExample([NotNull] ApiNode details) { return(PickExample(details, "Raw") ?? PickExample(details, "JS") ?? PickExample(details, "CS")); }
private void ParseFile(string filename, Version apiVersion, HashSet <string> processed) { if (processed.Contains(filename)) { return; } processed.Add(filename); // We're only interested in the file if it contains messages. Bail early // so we don't have to parse it to HTML var content = File.ReadAllText(filename); if (!content.Contains("Messages")) { return; } var document = ApiNode.LoadContent(content); var section = document?.SelectOne(@"//div.content/div.section"); var header = section?.SelectOne(@"div.mb20.clear"); var removed = header?.SelectOne(@"div[@class='message message-error mb20']"); var name = header?.SelectOne(@"h1.heading.inherit"); // Type or type member name var ns = header?.SelectOne(@"p"); // "class in {ns}"/"struct in {ns}"/"Namespace: {ns}" // Only interested in types at this point if (name == null || ns == null) { // Console.WriteLine("File has no types: {0}", filename); return; } // Only types that have messages var messages = section.Subsection("Messages").ToArray(); if (messages.Length == 0) { return; } var match = CaptureKindAndNamespaceRegex.Match(ns.Text); var clsType = match.Groups["type"].Value; var nsName = match.Groups["namespace"].Value; if (string.IsNullOrEmpty(clsType)) { clsType = "class"; } if (string.IsNullOrEmpty(nsName)) { // Quick fix up for the 5.0 docs, which don't specify a namespace for AssetModificationProcessor if (apiVersion == new Version(5, 0) && name.Text == "AssetModificationProcessor") { nsName = "UnityEditor"; } else { Console.WriteLine("Missing namespace: {0}", name.Text); return; } } if (removed != null && removed.Text.StartsWith("Removed")) { Console.WriteLine($"{nsName}.{name.Text} no longer available in {apiVersion}: {removed.Text}"); return; } var unityApiType = myApi.AddType(nsName, name.Text, clsType, filename, apiVersion); foreach (var message in messages) { var eventFunction = ParseMessage(name.Text, message, apiVersion, nsName, processed); unityApiType.MergeEventFunction(eventFunction, apiVersion); } }
// Gets return type and argument names from example private static Tuple<ApiType, string[]> ParseDetailsFromExample(string messageName, ApiNode example, string owningMessageNamespace) { var blankCleanup1 = new Regex(@"\s+"); var blankCleanup2 = new Regex(@"\s*(\W)\s*"); var exampleText = example.Text; exampleText = blankCleanup1.Replace(exampleText, " "); exampleText = blankCleanup2.Replace(exampleText, "$1"); var jsRegex = new Regex($@"(?:\W|^)function {messageName}\(([^)]*)\)(?::(\w+))?\{{"); var m = jsRegex.Match(exampleText); if (m.Success) { var returnType = new ApiType(m.Groups[2].Value, owningMessageNamespace); var parameters = m.Groups[1].Value.Split(','); var arguments = new string[parameters.Length]; for (var i = 0; i < parameters.Length; ++i) { arguments[i] = parameters[i].Split(':')[0]; } return Tuple.Create(returnType, arguments); } var csRegex = new Regex($@"(\w+) {messageName}\(([^)]*)\)"); m = csRegex.Match(exampleText); if (m.Success) { var nameRegex = new Regex(@"\W(\w+)$"); var returnType = new ApiType(m.Groups[1].Value, owningMessageNamespace); var parameters = m.Groups[2].Value.Split(','); var arguments = new string[parameters.Length]; for (var i = 0; i < parameters.Length; ++i) { arguments[i] = nameRegex.Replace(parameters[i], "$1"); } return Tuple.Create(returnType, arguments); } return null; }
private static ApiNode[] PickExample([NotNull] ApiNode details, [NotNull] string type) { return(details.SelectMany($@"div.subsection/pre.codeExample{type}")); }
private static Tuple <ApiType, string[], bool> ParseDetailsFromExample(string messageName, ApiNode example, string owningMessageNamespace) { // Grr. This took far too long to figure out... // The example for OnProjectChange uses "OnProjectChanged" instead // https://docs.unity3d.com/ScriptReference/EditorWindow.OnProjectChange.html if (messageName == "OnProjectChange" && example.Text.Contains("OnProjectChanged")) { messageName = "OnProjectChanged"; } var exampleText = example.Text; exampleText = SingleLineCommentsRegex.Replace(exampleText, string.Empty); exampleText = BlankCleanup1.Replace(exampleText, " "); exampleText = BlankCleanup2.Replace(exampleText, "$1"); exampleText = ArrayFixup.Replace(exampleText, "$1 $2"); // This matches both C# and JS function signatures var functionRegex = new Regex($@"(?:\W|^)(?<static>static\s+)?(?<returnType>\w+\W*)\s+{messageName}\((?<parameters>[^)]*)\)(?::(?<returnType>\w+\W*))?{{"); var m = functionRegex.Match(exampleText); if (m.Success) { var returnTypeName = m.Groups["returnType"].Value; if (returnTypeName == "function") // JS without an explicit return type { returnTypeName = "void"; } var returnType = new ApiType(returnTypeName, owningMessageNamespace); var parameters = m.Groups["parameters"].Value .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var isStatic = m.Groups["static"].Success; var arguments = new string[parameters.Length]; for (var i = 0; i < parameters.Length; ++i) { if (parameters[i].Contains(":")) { arguments[i] = parameters[i].Split(':')[0]; } else { arguments[i] = ParameterNameRegex.Replace(parameters[i], "$1"); } } return(Tuple.Create(returnType, arguments, isStatic)); } return(null); }
private bool ParseMessage([NotNull] ApiNode message, out string path, out ApiType type) { path = string.Empty; type = new ApiType("void"); var link = message.SelectOne(@"td.lbl/a"); var desc = message.SelectOne(@"td.desc"); if (link == null || desc == null) { return(false); } var detailsPath = link[@"href"]; if (string.IsNullOrWhiteSpace(detailsPath)) { return(false); } path = Path.Combine(rootPath, detailsPath); if (!File.Exists(path)) { return(false); } var detailsDoc = ApiNode.Load(path); var details = detailsDoc?.SelectOne(@"//div.content/div.section"); var signature = details?.SelectOne(@"div.mb20.clear/h1.heading.inherit"); var staticNode = details?.SelectOne(@"div.subsection/p/code.varname[text()='static']"); if (signature == null) { return(false); } api.Enter("message"); api.SetAttribute("name", link.Text); api.SetAttribute("static", staticNode != null); var description = desc.Text; if (!string.IsNullOrWhiteSpace(description)) { api.SetAttribute("description", description); } // E.g. OnCollisionExit2D(Collision2D) var argumentString = SigRegex.Replace(signature.Text, "$2$3"); if (string.IsNullOrWhiteSpace(argumentString)) { return(true); } var argumentStrings = argumentString.Split(',') .Select(s => s.Trim()) .ToArray(); var total = argumentStrings.Length; var arguments = argumentStrings.Select((s, i) => new Argument(s, i, total)).ToArray(); ResolveArguments(link.Text, details, arguments, ref type); api.Enter("parameters"); foreach (var argument in arguments) { api.Enter("parameter"); api.SetAttribute("type", argument.Type.FullName); api.SetAttribute("array", argument.Type.IsArray); api.SetAttribute("name", argument.Name); if (!string.IsNullOrWhiteSpace(argument.Description)) { api.SetAttribute("description", argument.Description); } } return(true); }
private static void ParseParameters(UnityApiEventFunction eventFunction, ApiNode signature, ApiNode details, string owningMessageNamespace, string[] argumentNames) { // E.g. OnCollisionExit2D(Collision2D) - doesn't always include the argument name // Hopefully, we parsed the argument name from the example var argumentString = SigRegex.Replace(signature.Text, "$2$3"); if (string.IsNullOrWhiteSpace(argumentString)) return; var argumentStrings = argumentString.Split(',') .Select(s => s.Trim()) .ToArray(); var total = argumentStrings.Length; var arguments = argumentStrings.Select((s, i) => new Argument(s, i, total, owningMessageNamespace)).ToArray(); ResolveArguments(details, arguments, argumentNames); foreach (var argument in arguments) eventFunction.AddParameter(argument.Name, argument.Type, argument.Description); }
private static ApiNode PickExample([NotNull] ApiNode details, [NotNull] string type) { var example = details.SelectOne($@"div.subsection/pre.codeExample{type}"); return(example == null || example.Text.StartsWith("no example available") ? null : example); }
// Gets return type and argument names from example private static Tuple <ApiType, string[]> ParseDetailsFromExample(string messageName, ApiNode example, string owningMessageNamespace) { var blankCleanup1 = new Regex(@"\s+"); var blankCleanup2 = new Regex(@"\s*(\W)\s*"); var exampleText = example.Text; exampleText = blankCleanup1.Replace(exampleText, " "); exampleText = blankCleanup2.Replace(exampleText, "$1"); var jsRegex = new Regex($@"(?:\W|^)function {messageName}\(([^)]*)\)(?::(\w+))?\{{"); var m = jsRegex.Match(exampleText); if (m.Success) { var returnType = new ApiType(m.Groups[2].Value, owningMessageNamespace); var parameters = m.Groups[1].Value.Split(','); var arguments = new string[parameters.Length]; for (var i = 0; i < parameters.Length; ++i) { arguments[i] = parameters[i].Split(':')[0]; } return(Tuple.Create(returnType, arguments)); } var csRegex = new Regex($@"(\w+) {messageName}\(([^)]*)\)"); m = csRegex.Match(exampleText); if (m.Success) { var nameRegex = new Regex(@"\W(\w+)$"); var returnType = new ApiType(m.Groups[1].Value, owningMessageNamespace); var parameters = m.Groups[2].Value.Split(','); var arguments = new string[parameters.Length]; for (var i = 0; i < parameters.Length; ++i) { arguments[i] = nameRegex.Replace(parameters[i], "$1"); } return(Tuple.Create(returnType, arguments)); } return(null); }
// Gets return type and argument names from example private static Tuple <ApiType, string[], bool> ParseDetailsFromExample(string messageName, ApiNode example, string owningMessageNamespace) { var blankCleanup1 = new Regex(@"\s+"); var blankCleanup2 = new Regex(@"\s*(\W)\s*"); var arrayFixup = new Regex(@"(\[\])(\w)"); var exampleText = example.Text; exampleText = blankCleanup1.Replace(exampleText, " "); exampleText = blankCleanup2.Replace(exampleText, "$1"); exampleText = arrayFixup.Replace(exampleText, "$1 $2"); var jsRegex = new Regex($@"(?:\W|^)(?<static>static\s+)?function {messageName}\((?<parameters>[^)]*)\)(?::(?<returnType>\w+\W*))?\{{"); var m = jsRegex.Match(exampleText); if (m.Success) { var returnType = new ApiType(m.Groups["returnType"].Value, owningMessageNamespace); var parameters = m.Groups["parameters"].Value.Split(','); var isStatic = m.Groups["static"].Success; var arguments = new string[parameters.Length]; for (var i = 0; i < parameters.Length; ++i) { arguments[i] = parameters[i].Split(':')[0]; } return(Tuple.Create(returnType, arguments, isStatic)); } var csRegex = new Regex($@"(?:\W|^)(?<static>static\s+)?(?<returnType>\w+\W*) {messageName}\((?<parameters>[^)]*)\)"); m = csRegex.Match(exampleText); if (m.Success) { var nameRegex = new Regex(@"^.*?\W(\w+)$"); var returnType = new ApiType(m.Groups["returnType"].Value, owningMessageNamespace); var parameters = m.Groups["parameters"].Value.Split(','); var isStatic = m.Groups["static"].Success; var arguments = new string[parameters.Length]; for (var i = 0; i < parameters.Length; ++i) { arguments[i] = nameRegex.Replace(parameters[i], "$1"); } return(Tuple.Create(returnType, arguments, isStatic)); } return(null); }
private UnityApiEventFunction ParseMessage(string className, ApiNode message, Version apiVersion, string hintNamespace, HashSet <string> processed) { var link = message.SelectOne(@"td.lbl/a"); var desc = message.SelectOne(@"td.desc"); if (link == null || desc == null) { return(null); } var detailsPath = link[@"href"]; if (string.IsNullOrWhiteSpace(detailsPath)) { return(null); } var scriptReferenceRelativePath = Directory.Exists(ScriptReferenceRelativePath1) ? ScriptReferenceRelativePath1 : ScriptReferenceRelativePath2; var path = Path.Combine(scriptReferenceRelativePath, detailsPath); processed.Add(path); if (!File.Exists(path)) { return(null); } var detailsDoc = ApiNode.Load(path); var details = detailsDoc?.SelectOne(@"//div.content/div.section"); var signature = details?.SelectOne(@"div.mb20.clear/h1.heading.inherit"); var staticNode = details?.SelectOne(@"div.subsection/p/code.varname[text()='static']"); if (signature == null) { return(null); } var isCoroutine = IsCoroutineRegex.IsMatch(details.Text); var messageName = link.Text; var returnType = ApiType.Void; var argumentNames = EmptyArray <string> .Instance; var isStaticFromExample = false; var examples = PickExample(details); if (examples.Length > 0) { var tuple = ParseDetailsFromExample(messageName, examples, hintNamespace); // As of 2017.4, the docs for MonoBehaviour.OnCollisionEnter2D don't include a valid example. It demonstrates // OnTriggerEnter2D instead. Similar problems for these other methods if (tuple == null) { var fullName = $"{className}.{messageName}"; switch (fullName) { case "MonoBehaviour.OnCollisionEnter2D": case "MonoBehaviour.OnCollisionExit2D": case "MonoBehaviour.OnCollisionStay2D": case "MonoBehaviour.Start": case "MonoBehaviour.OnDestroy": Console.WriteLine( $"WARNING: Unable to parse example for {fullName}. Example incorrect in docs"); break; // case "Network.OnDisconnectedFromServer": // Bug in 2018.2 documentation // Console.WriteLine($"WARNING: Missing example for {fullName}"); // break; default: foreach (var example in examples) { Console.WriteLine(example.Text); Console.WriteLine(); } throw new InvalidOperationException($"Failed to parse example for {className}.{messageName}"); } } if (tuple != null) { returnType = tuple.Item1; argumentNames = tuple.Item2; isStaticFromExample = tuple.Item3; } } if (Equals(returnType, ApiType.IEnumerator)) { returnType = ApiType.Void; isCoroutine = true; } var docPath = Path.Combine(scriptReferenceRelativePath, detailsPath); var eventFunction = new UnityApiEventFunction(messageName, staticNode != null || isStaticFromExample, isCoroutine, returnType, apiVersion, desc.Text, docPath); ParseParameters(eventFunction, signature, details, hintNamespace, argumentNames); return(eventFunction); }
private static ApiNode PickExample([NotNull] ApiNode details) { // Favour C#, it's the most strongly typed return(PickExample(details, "CS") ?? PickExample(details, "JS") ?? PickExample(details, "Raw")); }
private UnityApiEventFunction ParseMessage(ApiNode message, Version apiVersion, string hintNamespace) { var link = message.SelectOne(@"td.lbl/a"); var desc = message.SelectOne(@"td.desc"); if (link == null || desc == null) return null; var detailsPath = link[@"href"]; if (string.IsNullOrWhiteSpace(detailsPath)) return null; var path = Path.Combine(myScriptReferenceRelativePath, detailsPath); if (!File.Exists(path)) return null; var detailsDoc = ApiNode.Load(path); var details = detailsDoc?.SelectOne(@"//div.content/div.section"); var signature = details?.SelectOne(@"div.mb20.clear/h1.heading.inherit"); var staticNode = details?.SelectOne(@"div.subsection/p/code.varname[text()='static']"); if (signature == null) return null; var messageName = link.Text; var returnType = ApiType.Void; string[] argumentNames = null; var example = PickExample(details); if (example != null) { var tuple = ParseDetailsFromExample(messageName, example, hintNamespace); returnType = tuple.Item1; argumentNames = tuple.Item2; } var docPath = Path.Combine(myScriptReferenceRelativePath, detailsPath); var eventFunction = new UnityApiEventFunction(messageName, staticNode != null, returnType, apiVersion, desc.Text, docPath, false); ParseParameters(eventFunction, signature, details, hintNamespace, argumentNames); return eventFunction; }