/* Function: BuildDataFiles * Generates the output data file for the container. It must have <JSONContainer.DataFileName> set. If it finds any * sub-containers that also have that set, it will recursively generate files for them as well. */ protected void BuildDataFiles(JSONMenuEntries.Container container) { #if DEBUG if (container.StartsNewDataFile == false) { throw new Exception("BuildOutput() can only be called on containers with DataFileName set."); } #endif Stack <JSONMenuEntries.Container> containersToBuild = new Stack <JSONMenuEntries.Container>(); containersToBuild.Push(container); while (containersToBuild.Count > 0) { var containerToBuild = containersToBuild.Pop(); string fileName = containerToBuild.DataFileName; StringBuilder output = new StringBuilder(); output.Append("NDMenu.OnSectionLoaded(\""); output.StringEscapeAndAppend(fileName); output.Append("\",["); if (addWhitespace) { output.AppendLine(); } AppendMembers(containerToBuild, output, 1, containersToBuild); if (addWhitespace) { output.Append(' ', IndentWidth); } output.Append("]);"); WriteTextFile(Paths.Menu.OutputFolder(Target.OutputFolder) + "/" + fileName, output.ToString()); } }
/* Function: AppendMembers * A support function for <BuildDataFile()>. Appends the output of the container's members to the string, recursively going * through sub-containers as well. This will not include the surrounding brackets, only the comma-separated member entries. * If it finds any sub-containers that start a new data file, it will add them to containersToBuild. */ protected void AppendMembers(JSONMenuEntries.Container container, StringBuilder output, int indent, Stack <JSONMenuEntries.Container> containersToBuild) { for (int i = 0; i < container.Members.Count; i++) { var member = container.Members[i]; if (addWhitespace) { output.Append(' ', indent * IndentWidth); } if (member is JSONMenuEntries.Container) { var memberContainer = (JSONMenuEntries.Container)member; output.Append(memberContainer.JSONBeforeMembers); if (memberContainer.StartsNewDataFile) { output.Append('"'); output.StringEscapeAndAppend(memberContainer.DataFileName); output.Append('"'); containersToBuild.Push(memberContainer); } else { output.Append('['); if (addWhitespace) { output.AppendLine(); } AppendMembers(memberContainer, output, indent + 1, containersToBuild); if (addWhitespace) { output.Append(' ', (indent + 1) * IndentWidth); } output.Append(']'); } output.Append(memberContainer.JSONAfterMembers); } else // not a container { var memberTarget = (JSONMenuEntries.Target)member; output.Append(memberTarget.JSON); } if (i < container.Members.Count - 1) { output.Append(','); } if (addWhitespace) { output.AppendLine(); } } }
/* Function: AssignDataFiles * * Segments the menu into smaller pieces and generates data file names. * * Parameters: * * container - The container to segment. This will always be assigned a data file name. * usedDataFiles - A table mapping each <Hierarchy> to the data file numbers already in use for it, such as Files -> {1-4}. * It will be used to determine which numbers are available to assign, and new numbers will be added to it * as they are assigned by this function. */ protected void AssignDataFiles(JSONMenuEntries.Container container, ref NumberSetTable <Hierarchy> usedDataFiles) { // Generate the data file name for this container. Hierarchy hierarchy = container.MenuEntry.Hierarchy; int dataFileNumber = usedDataFiles.LowestAvailable(hierarchy); usedDataFiles.Add(hierarchy, dataFileNumber); container.DataFileName = Paths.Menu.OutputFile(Target.OutputFolder, hierarchy, dataFileNumber, fileNameOnly: true); // The data file has to include all the members in this container no matter what, so we don't check the size against the limit // yet. int containerJSONSize = container.JSONBeforeMembers.Length + container.JSONAfterMembers.Length + container.JSONLengthOfMembers; // Now find all the subcontainers, which are now candidates for inlining. List <JSONMenuEntries.Container> inliningCandidates = null; foreach (var member in container.Members) { if (member is JSONMenuEntries.Container) { var containerMember = (JSONMenuEntries.Container)member; if (inliningCandidates == null) { inliningCandidates = new List <JSONMenuEntries.Container>(); } inliningCandidates.Add(containerMember); } } // If there's no subcontainers we're done. if (inliningCandidates == null) { return; } // Go through all our candidates and inline them smallest to largest. This prevents one very large container early in the list // from causing all the other ones to be broken out into separate files. // Keep track of which containers were inlined so we can possibly inline their members as well. List <JSONMenuEntries.Container> inlinedContainers = new List <JSONMenuEntries.Container>(); while (inliningCandidates.Count > 0) { // Find the smallest of the candidates int smallestInliningCandidateIndex = 0; int smallestInliningCandidateSize = inliningCandidates[0].JSONLengthOfMembers; for (int i = 1; i < inliningCandidates.Count; i++) { if (inliningCandidates[i].JSONLengthOfMembers < smallestInliningCandidateSize) { smallestInliningCandidateIndex = i; smallestInliningCandidateSize = inliningCandidates[i].JSONLengthOfMembers; } } // If the smallest candidate fits into the segment length limits, inline it if (containerJSONSize + smallestInliningCandidateSize <= SegmentLength) { containerJSONSize += smallestInliningCandidateSize; inlinedContainers.Add(inliningCandidates[smallestInliningCandidateIndex]); inliningCandidates.RemoveAt(smallestInliningCandidateIndex); } // If the smallest candidate doesn't fit, that means it and all the remaining candidates need to get their own files else { foreach (var inliningCandidate in inliningCandidates) { AssignDataFiles(inliningCandidate, ref usedDataFiles); } inliningCandidates.Clear(); } // If there's no more candidates, go through the list of inlined containers and add their subcontainers to the candidates // list. This allows us to continue inlining for multiple levels as long as we have space for it. // This algorithm causes inlining to happen breadth-first instead of depth-first, which we want, but it also allows lower // depths to continue to be inlined even if the parent level couldn't be done completely. It's possible that when there's // no room for all the top-level containers a few more lower level ones could still be squeezed in. if (inliningCandidates.Count == 0 && inlinedContainers.Count > 0) { foreach (var inlinedContainer in inlinedContainers) { foreach (var member in inlinedContainer.Members) { if (member is JSONMenuEntries.Container) { inliningCandidates.Add((JSONMenuEntries.Container)member); } } } inlinedContainers.Clear(); } } }
/* Function: ConvertToJSON * Converts a Container menu entry to JSON, along with all of its members. This is a recursive function so it will convert * the entire tree inside the container. */ protected JSONMenuEntries.Container ConvertToJSON(MenuEntries.Container menuContainer) { JSONMenuEntries.Container jsonContainer = new JSONMenuEntries.Container(menuContainer); StringBuilder jsonBeforeMembers = new StringBuilder(); jsonBeforeMembers.Append("[2,"); // Title if (menuContainer.CondensedTitles == null) { if (menuContainer.Title != null) { jsonBeforeMembers.Append('"'); jsonBeforeMembers.StringEscapeAndAppend(menuContainer.Title.ToHTML()); jsonBeforeMembers.Append('"'); } // Otherwise leave an empty space before the comma. We don't have to write out "undefined". } else { jsonBeforeMembers.Append("[\""); jsonBeforeMembers.StringEscapeAndAppend(menuContainer.Title.ToHTML()); jsonBeforeMembers.Append('"'); foreach (string condensedTitle in menuContainer.CondensedTitles) { jsonBeforeMembers.Append(",\""); jsonBeforeMembers.StringEscapeAndAppend(condensedTitle.ToHTML()); jsonBeforeMembers.Append('"'); } jsonBeforeMembers.Append(']'); } // Hash path jsonBeforeMembers.Append(','); string hashPath = null; if (menuContainer is MenuEntries.FileSource) { var fileSourceEntry = (MenuEntries.FileSource)menuContainer; hashPath = Paths.SourceFile.FolderHashPath(fileSourceEntry.WrappedFileSource.Number, fileSourceEntry.CondensedPathFromFileSource); } else if (menuContainer is MenuEntries.Folder) { var folderEntry = (MenuEntries.Folder)menuContainer; // Walk up the tree until you find the FileSource MenuEntries.Container parentEntry = menuContainer.Parent; #if DEBUG if (parentEntry == null) { throw new Exception("Parent must be defined when generating JSON for menu folder \"" + (folderEntry.Title ?? "") + "\"."); } #endif while ((parentEntry is MenuEntries.FileSource) == false) { parentEntry = parentEntry.Parent; #if DEBUG if (parentEntry == null) { throw new Exception("Couldn't find a file source among the folder \"" + (folderEntry.Title ?? "") + "\"'s parents when generating JSON."); } #endif } var fileSourceEntry = (MenuEntries.FileSource)parentEntry; hashPath = Paths.SourceFile.FolderHashPath(fileSourceEntry.WrappedFileSource.Number, folderEntry.PathFromFileSource); } else if (menuContainer is MenuEntries.Language) { var languageEntry = (MenuEntries.Language)menuContainer; hashPath = Paths.Class.QualifierHashPath(languageEntry.WrappedLanguage.SimpleIdentifier, languageEntry.CondensedScopeString); } else if (menuContainer is MenuEntries.Scope) { var scopeEntry = (MenuEntries.Scope)menuContainer; if (scopeEntry.Hierarchy == Hierarchy.Class) { // Walk up the tree until you find the language MenuEntries.Container parentEntry = menuContainer.Parent; #if DEBUG if (parentEntry == null) { throw new Exception("Parent must be defined when generating JSON for menu scope \"" + (scopeEntry.Title ?? "") + "\"."); } #endif while ((parentEntry is MenuEntries.Language) == false) { parentEntry = parentEntry.Parent; #if DEBUG if (parentEntry == null) { throw new Exception("Couldn't find a language among the scope \"" + (scopeEntry.Title ?? "") + "\"'s parents when generating JSON."); } #endif } var languageEntry = (MenuEntries.Language)parentEntry; hashPath = Paths.Class.QualifierHashPath(languageEntry.WrappedLanguage.SimpleIdentifier, scopeEntry.WrappedScopeString); } else if (scopeEntry.Hierarchy == Hierarchy.Database) { hashPath = Paths.Database.QualifierHashPath(scopeEntry.WrappedScopeString); } else { throw new NotImplementedException(); } } // If we're at one of the menu roots else if (menuContainer.Parent == null) { if (menuContainer.Hierarchy == Hierarchy.File || menuContainer.Hierarchy == Hierarchy.Class) { // If we're at a root file or class container that is not also a language or file source, it means there are multiple // languages and/or file sources beneath it and thus there is no shared hash path. "CSharpClass:" and "PerlClass:", // "Files:" and "Files2:", etc. hashPath = null; } else if (menuContainer.Hierarchy == Hierarchy.Database) { // If we're at the root database menu and the entry is not also a scope, it means there are multiple scopes beneath it. // However, unlike files and classes, there is still the shared "Database:" hash path. hashPath = Paths.Database.QualifierHashPath(); } else { throw new NotImplementedException(); } } else { throw new NotImplementedException(); } if (hashPath != null) { jsonBeforeMembers.Append('"'); jsonBeforeMembers.StringEscapeAndAppend(hashPath); jsonBeforeMembers.Append('"'); } // Otherwise leave an empty space before the comma. We don't have to write out "undefined". jsonBeforeMembers.Append(','); jsonContainer.JSONBeforeMembers = jsonBeforeMembers.ToString(); jsonContainer.JSONAfterMembers = "]"; jsonContainer.HashPath = hashPath; // Now recurse into members foreach (var member in menuContainer.Members) { jsonContainer.Members.Add(ConvertToJSON(member, jsonContainer.HashPath)); } return(jsonContainer); }