// Group: Functions // _________________________________________________________________________ public ContainerExtraData(MenuEntries.Base.Container menuEntry) { this.menuEntry = menuEntry; this.jsonBeforeMembers = null; this.jsonAfterMembers = null; this.jsonLengthOfMembers = -1; this.dataFileName = null; this.hashPath = null; }
/* Function: BuildOutput * Generates the output file for the container. It must have <ContainerExtraData.DataFileName> set. If it finds * any sub-containers that also have that set, it will recursively generate files for them as well. */ protected void BuildOutput(MenuEntries.Base.Container container) { #if DEBUG if (container.ExtraData == null || (container.ExtraData as ContainerExtraData).StartsNewDataFile == false) { throw new Exception("BuildOutput() can only be called on containers with DataFileName set."); } #endif Stack <MenuEntries.Base.Container> containersToBuild = new Stack <MenuEntries.Base.Container>(); containersToBuild.Push(container); while (containersToBuild.Count > 0) { MenuEntries.Base.Container containerToBuild = containersToBuild.Pop(); string fileName = (containerToBuild.ExtraData as ContainerExtraData).DataFileName; StringBuilder output = new StringBuilder(); output.Append("NDMenu.OnSectionLoaded(\""); output.StringEscapeAndAppend(fileName); output.Append("\",["); if (!EngineInstance.Config.ShrinkFiles) { output.AppendLine(); } AppendMembers(containerToBuild, output, 1, containersToBuild); if (!EngineInstance.Config.ShrinkFiles) { output.Append(' ', IndentSpaces); } output.Append("]);"); System.IO.File.WriteAllText(HTMLBuilder.Menu_DataFolder + "/" + fileName, output.ToString()); } }
/* Function: GenerateJSON * Generates JSON for all the entries in the passed container. */ protected void GenerateJSON(MenuEntries.Base.Container container) { ContainerExtraData containerExtraData = new ContainerExtraData(container); container.ExtraData = containerExtraData; containerExtraData.GenerateJSON(HTMLBuilder, this); foreach (var member in container.Members) { if (member is MenuEntries.Base.Target) { TargetExtraData targetExtraData = new TargetExtraData((MenuEntries.Base.Target)member); member.ExtraData = targetExtraData; targetExtraData.GenerateJSON(HTMLBuilder, this); } else if (member is MenuEntries.Base.Container) { GenerateJSON((MenuEntries.Base.Container)member); } } }
/* Function: GenerateJSON */ public void GenerateJSON(Builders.HTML htmlBuilder, JSMenuData menu) { StringBuilder output = new StringBuilder(); output.Append("[2,"); // Title if (menuEntry.CondensedTitles == null) { if (menuEntry.Title != null) { output.Append('"'); output.StringEscapeAndAppend(menuEntry.Title.ToHTML()); output.Append('"'); } // Otherwise leave an empty space before the comma. We don't have to write out "undefined". } else { output.Append("[\""); output.StringEscapeAndAppend(menuEntry.Title.ToHTML()); output.Append('"'); foreach (string condensedTitle in menuEntry.CondensedTitles) { output.Append(",\""); output.StringEscapeAndAppend(condensedTitle.ToHTML()); output.Append('"'); } output.Append(']'); } // Hash path output.Append(','); if (menuEntry is MenuEntries.Files.FileSource) { MenuEntries.Files.FileSource fileSourceEntry = (MenuEntries.Files.FileSource)menuEntry; hashPath = htmlBuilder.Source_OutputFolderHashPath(fileSourceEntry.WrappedFileSource.Number, fileSourceEntry.CondensedPathFromFileSource); } else if (menuEntry is MenuEntries.Files.Folder) { MenuEntries.Base.Container container = menuEntry.Parent; #if DEBUG if (container == null) { throw new Exception("Parent must be defined when generating JSON for menu folder \"" + menuEntry.Title + "\"."); } #endif while ((container is MenuEntries.Files.FileSource) == false) { container = container.Parent; #if DEBUG if (container == null) { throw new Exception("Couldn't find a file source among the folder \"" + menuEntry.Title + "\"'s parents when generating JSON."); } #endif } MenuEntries.Files.Folder folderEntry = (MenuEntries.Files.Folder)menuEntry; MenuEntries.Files.FileSource fileSourceEntry = (MenuEntries.Files.FileSource)container; hashPath = htmlBuilder.Source_OutputFolderHashPath(fileSourceEntry.WrappedFileSource.Number, folderEntry.PathFromFileSource); } else if (menuEntry is MenuEntries.Classes.Language) { MenuEntries.Classes.Language languageEntry = (MenuEntries.Classes.Language)menuEntry; hashPath = htmlBuilder.Class_OutputFolderHashPath(languageEntry.WrappedLanguage, languageEntry.CondensedScopeString); } else if (menuEntry is MenuEntries.Classes.Scope) { MenuEntries.Base.Container container = menuEntry; #if DEBUG if (container == null) { throw new Exception("Parent must be defined when generating JSON for menu scope \"" + menuEntry.Title + "\"."); } #endif while ((container is MenuEntries.Classes.Language) == false && container != menu.RootDatabaseMenu) { container = container.Parent; #if DEBUG if (container == null) { throw new Exception("Couldn't find a language among the scope \"" + menuEntry.Title + "\"'s parents when generating JSON."); } #endif } MenuEntries.Classes.Scope scopeEntry = (MenuEntries.Classes.Scope)menuEntry; if (container == menu.RootDatabaseMenu) { hashPath = htmlBuilder.Database_OutputFolderHashPath(scopeEntry.WrappedScopeString); } else { MenuEntries.Classes.Language languageEntry = (MenuEntries.Classes.Language)container; hashPath = htmlBuilder.Class_OutputFolderHashPath(languageEntry.WrappedLanguage, scopeEntry.WrappedScopeString); } } else if (menuEntry == menu.RootFileMenu || menuEntry == menu.RootClassMenu) { // If we're at the root file or class menu and the entry 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 (menuEntry == menu.RootDatabaseMenu) { // 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 = htmlBuilder.Database_OutputFolderHashPath(); } #if DEBUG else { throw new Exception("Don't know how to generate JSON for container \"" + menuEntry.Title + "\"."); } #endif if (hashPath != null) { output.Append('"'); output.StringEscapeAndAppend(hashPath); output.Append('"'); } // Otherwise leave an empty space before the comma. We don't have to write out "undefined". output.Append(','); jsonBeforeMembers = output.ToString(); jsonAfterMembers = "]"; }
/* Function: AppendMembers * A support function for <BuildOutput()>. 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(MenuEntries.Base.Container container, StringBuilder output, int indent, Stack <MenuEntries.Base.Container> containersToBuild) { for (int i = 0; i < container.Members.Count; i++) { var member = container.Members[i]; if (!EngineInstance.Config.ShrinkFiles) { output.Append(' ', indent * IndentSpaces); } if (member is MenuEntries.Base.Target) { TargetExtraData targetExtraData = (TargetExtraData)member.ExtraData; output.Append(targetExtraData.JSON); } else if (member is MenuEntries.Base.Container) { ContainerExtraData containerExtraData = (ContainerExtraData)member.ExtraData; output.Append(containerExtraData.JSONBeforeMembers); if (containerExtraData.StartsNewDataFile) { output.Append('"'); output.StringEscapeAndAppend(containerExtraData.DataFileName); output.Append('"'); containersToBuild.Push((MenuEntries.Base.Container)member); } else { output.Append('['); if (!EngineInstance.Config.ShrinkFiles) { output.AppendLine(); } AppendMembers((MenuEntries.Base.Container)member, output, indent + 1, containersToBuild); if (!EngineInstance.Config.ShrinkFiles) { output.Append(' ', (indent + 1) * IndentSpaces); } output.Append(']'); } output.Append(containerExtraData.JSONAfterMembers); } #if DEBUG else { throw new Exception("Can't append JSON for menu entry " + member.Title + "."); } #endif if (i < container.Members.Count - 1) { output.Append(','); } if (!EngineInstance.Config.ShrinkFiles) { output.AppendLine(); } } }
/* Function: SegmentMenu * Segments the menu into smaller pieces and generates data file names. */ protected void SegmentMenu(MenuEntries.Base.Container container, string dataFileType, ref StringTable <IDObjects.NumberSet> usedDataFiles) { // Generate the data file name for this container. IDObjects.NumberSet usedDataFileNumbers = usedDataFiles[dataFileType]; if (usedDataFileNumbers == null) { usedDataFileNumbers = new IDObjects.NumberSet(); usedDataFiles.Add(dataFileType, usedDataFileNumbers); } int dataFileNumber = usedDataFileNumbers.LowestAvailable; usedDataFileNumbers.Add(dataFileNumber); ContainerExtraData extraData = (ContainerExtraData)container.ExtraData; extraData.DataFileName = HTMLBuilder.Menu_DataFileNameOnly(dataFileType, dataFileNumber); // The data file has to include all the members in this container no matter what. int containerJSONSize = extraData.JSONBeforeMembers.Length + extraData.JSONAfterMembers.Length + extraData.JSONLengthOfMembers; List <MenuEntries.Base.Container> subContainers = null; foreach (var member in container.Members) { if (member is MenuEntries.Base.Container) { if (subContainers == null) { subContainers = new List <MenuEntries.Base.Container>(); } subContainers.Add((MenuEntries.Base.Container)member); } } // Now start including the contents of subcontainers until we reach the size limit. We're going breadth-first instead of // depth first. List <MenuEntries.Base.Container> nextSubContainers = null; for (;;) { if (subContainers == null || subContainers.Count == 0) { if (nextSubContainers == null || nextSubContainers.Count == 0) { break; } else { subContainers = nextSubContainers; nextSubContainers = null; } } // Add subcontainers to the file in the order from 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. // DEPENDENCY: ContainerExtraData.JSONLengthOfMembers must cache its value for this algorithm to be efficient. int smallestSubContainerIndex = 0; int smallestSubContainerSize = (subContainers[0].ExtraData as ContainerExtraData).JSONLengthOfMembers; for (int i = 1; i < subContainers.Count; i++) { if ((subContainers[i].ExtraData as ContainerExtraData).JSONLengthOfMembers < smallestSubContainerSize) { smallestSubContainerIndex = i; smallestSubContainerSize = (subContainers[i].ExtraData as ContainerExtraData).JSONLengthOfMembers; } } containerJSONSize += smallestSubContainerSize; if (containerJSONSize > SegmentLength) { break; } foreach (var member in subContainers[smallestSubContainerIndex].Members) { if (member is MenuEntries.Base.Container) { if (nextSubContainers == null) { nextSubContainers = new List <MenuEntries.Base.Container>(); } nextSubContainers.Add((MenuEntries.Base.Container)member); } } subContainers.RemoveAt(smallestSubContainerIndex); } // Now recurse through any remaining subcontainers so they get their own files. if (subContainers != null) { foreach (var subContainer in subContainers) { SegmentMenu(subContainer, dataFileType, ref usedDataFiles); } } if (nextSubContainers != null) { foreach (var subContainer in nextSubContainers) { SegmentMenu(subContainer, dataFileType, ref usedDataFiles); } } }