bixbite 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +49 -0
  3. data/VERSION +1 -0
  4. data/bin/bixbite +73 -0
  5. data/lib/bixbite.rb +13 -0
  6. data/lib/bixbite/command.rb +14 -0
  7. data/lib/bixbite/create.rb +76 -0
  8. data/template/Rakefile +25 -0
  9. data/template/assets/bixbite/Rakefile.rb +297 -0
  10. data/template/assets/naturaldocs/NaturalDocs/Config/Languages.txt +286 -0
  11. data/template/assets/naturaldocs/NaturalDocs/Config/Topics.txt +382 -0
  12. data/template/assets/naturaldocs/NaturalDocs/Help/customizinglanguages.html +52 -0
  13. data/template/assets/naturaldocs/NaturalDocs/Help/customizingtopics.html +74 -0
  14. data/template/assets/naturaldocs/NaturalDocs/Help/documenting.html +58 -0
  15. data/template/assets/naturaldocs/NaturalDocs/Help/documenting/reference.html +146 -0
  16. data/template/assets/naturaldocs/NaturalDocs/Help/documenting/walkthrough.html +180 -0
  17. data/template/assets/naturaldocs/NaturalDocs/Help/example/Default.css +528 -0
  18. data/template/assets/naturaldocs/NaturalDocs/Help/example/NaturalDocs.js +204 -0
  19. data/template/assets/naturaldocs/NaturalDocs/Help/examples.css +90 -0
  20. data/template/assets/naturaldocs/NaturalDocs/Help/images/header/background.png +0 -0
  21. data/template/assets/naturaldocs/NaturalDocs/Help/images/header/leftside.png +0 -0
  22. data/template/assets/naturaldocs/NaturalDocs/Help/images/header/logo.png +0 -0
  23. data/template/assets/naturaldocs/NaturalDocs/Help/images/header/overbody.png +0 -0
  24. data/template/assets/naturaldocs/NaturalDocs/Help/images/header/overbodybg.png +0 -0
  25. data/template/assets/naturaldocs/NaturalDocs/Help/images/header/overleftmargin.png +0 -0
  26. data/template/assets/naturaldocs/NaturalDocs/Help/images/header/overmenu.png +0 -0
  27. data/template/assets/naturaldocs/NaturalDocs/Help/images/header/overmenubg.png +0 -0
  28. data/template/assets/naturaldocs/NaturalDocs/Help/images/header/rightside.png +0 -0
  29. data/template/assets/naturaldocs/NaturalDocs/Help/images/logo.gif +0 -0
  30. data/template/assets/naturaldocs/NaturalDocs/Help/images/menu/about.png +0 -0
  31. data/template/assets/naturaldocs/NaturalDocs/Help/images/menu/background.png +0 -0
  32. data/template/assets/naturaldocs/NaturalDocs/Help/images/menu/bottomleft.png +0 -0
  33. data/template/assets/naturaldocs/NaturalDocs/Help/images/menu/bottomright.png +0 -0
  34. data/template/assets/naturaldocs/NaturalDocs/Help/images/menu/community.png +0 -0
  35. data/template/assets/naturaldocs/NaturalDocs/Help/images/menu/customizing.png +0 -0
  36. data/template/assets/naturaldocs/NaturalDocs/Help/images/menu/using.png +0 -0
  37. data/template/assets/naturaldocs/NaturalDocs/Help/index.html +9 -0
  38. data/template/assets/naturaldocs/NaturalDocs/Help/javascript/BrowserStyles.js +77 -0
  39. data/template/assets/naturaldocs/NaturalDocs/Help/javascript/PNGHandling.js +72 -0
  40. data/template/assets/naturaldocs/NaturalDocs/Help/keywords.html +38 -0
  41. data/template/assets/naturaldocs/NaturalDocs/Help/languages.html +32 -0
  42. data/template/assets/naturaldocs/NaturalDocs/Help/menu.html +79 -0
  43. data/template/assets/naturaldocs/NaturalDocs/Help/output.html +84 -0
  44. data/template/assets/naturaldocs/NaturalDocs/Help/running.html +40 -0
  45. data/template/assets/naturaldocs/NaturalDocs/Help/styles.css +290 -0
  46. data/template/assets/naturaldocs/NaturalDocs/Help/styles.html +52 -0
  47. data/template/assets/naturaldocs/NaturalDocs/Help/troubleshooting.html +18 -0
  48. data/template/assets/naturaldocs/NaturalDocs/Info/CSSGuide.txt +947 -0
  49. data/template/assets/naturaldocs/NaturalDocs/Info/File Parsing.txt +83 -0
  50. data/template/assets/naturaldocs/NaturalDocs/Info/HTMLTestCases.pm +269 -0
  51. data/template/assets/naturaldocs/NaturalDocs/Info/Languages.txt +107 -0
  52. data/template/assets/naturaldocs/NaturalDocs/Info/NDMarkup.txt +91 -0
  53. data/template/assets/naturaldocs/NaturalDocs/Info/Symbol Management.txt +59 -0
  54. data/template/assets/naturaldocs/NaturalDocs/Info/images/Logo.png +0 -0
  55. data/template/assets/naturaldocs/NaturalDocs/JavaScript/NaturalDocs.js +836 -0
  56. data/template/assets/naturaldocs/NaturalDocs/License-GPL.txt +341 -0
  57. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/BinaryFile.pm +294 -0
  58. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Builder.pm +280 -0
  59. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Builder/Base.pm +348 -0
  60. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Builder/FramedHTML.pm +345 -0
  61. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Builder/HTML.pm +398 -0
  62. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Builder/HTMLBase.pm +3693 -0
  63. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/ClassHierarchy.pm +860 -0
  64. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/ClassHierarchy/Class.pm +412 -0
  65. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/ClassHierarchy/File.pm +157 -0
  66. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/ConfigFile.pm +497 -0
  67. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Constants.pm +165 -0
  68. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/DefineMembers.pm +100 -0
  69. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Error.pm +305 -0
  70. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/File.pm +540 -0
  71. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/ImageReferenceTable.pm +383 -0
  72. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/ImageReferenceTable/Reference.pm +44 -0
  73. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/ImageReferenceTable/String.pm +110 -0
  74. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages.pm +1475 -0
  75. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/ActionScript.pm +1473 -0
  76. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Ada.pm +38 -0
  77. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Advanced.pm +828 -0
  78. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Advanced/Scope.pm +95 -0
  79. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Advanced/ScopeChange.pm +70 -0
  80. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Base.pm +832 -0
  81. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/CSharp.pm +1484 -0
  82. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/PLSQL.pm +319 -0
  83. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Pascal.pm +143 -0
  84. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Perl.pm +1370 -0
  85. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Prototype.pm +92 -0
  86. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Prototype/Parameter.pm +87 -0
  87. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Simple.pm +503 -0
  88. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Languages/Tcl.pm +219 -0
  89. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Menu.pm +3406 -0
  90. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Menu/Entry.pm +201 -0
  91. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/NDMarkup.pm +76 -0
  92. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Parser.pm +1331 -0
  93. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Parser/JavaDoc.pm +464 -0
  94. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Parser/Native.pm +1060 -0
  95. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Parser/ParsedTopic.pm +253 -0
  96. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Project.pm +1402 -0
  97. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Project/ImageFile.pm +160 -0
  98. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Project/SourceFile.pm +113 -0
  99. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/ReferenceString.pm +334 -0
  100. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Settings.pm +1418 -0
  101. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Settings/BuildTarget.pm +66 -0
  102. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SourceDB.pm +678 -0
  103. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SourceDB/Extension.pm +84 -0
  104. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SourceDB/File.pm +129 -0
  105. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SourceDB/Item.pm +201 -0
  106. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SourceDB/ItemDefinition.pm +45 -0
  107. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SourceDB/WatchedFileDefinitions.pm +159 -0
  108. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/StatusMessage.pm +102 -0
  109. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SymbolString.pm +212 -0
  110. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SymbolTable.pm +1984 -0
  111. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SymbolTable/File.pm +186 -0
  112. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SymbolTable/IndexElement.pm +522 -0
  113. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SymbolTable/Reference.pm +273 -0
  114. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SymbolTable/ReferenceTarget.pm +97 -0
  115. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SymbolTable/Symbol.pm +428 -0
  116. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/SymbolTable/SymbolDefinition.pm +96 -0
  117. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Topics.pm +1319 -0
  118. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Topics/Type.pm +151 -0
  119. data/template/assets/naturaldocs/NaturalDocs/Modules/NaturalDocs/Version.pm +384 -0
  120. data/template/assets/naturaldocs/NaturalDocs/NaturalDocs +400 -0
  121. data/template/assets/naturaldocs/NaturalDocs/NaturalDocs.bat +17 -0
  122. data/template/assets/naturaldocs/NaturalDocs/Styles/Default.css +767 -0
  123. data/template/assets/naturaldocs/NaturalDocs/Styles/Roman.css +765 -0
  124. data/template/assets/naturaldocs/NaturalDocs/Styles/Small.css +763 -0
  125. data/template/assets/utilities/pngout +0 -0
  126. data/template/deploy/public_html/.htaccess +0 -0
  127. data/template/documentation/js/.htaccess +0 -0
  128. data/template/src/html/.htaccess +76 -0
  129. data/template/src/html/css/cmn/global.css +96 -0
  130. data/template/src/html/css/cmn/ie.css +15 -0
  131. data/template/src/html/css/cmn/ie6.css +15 -0
  132. data/template/src/html/images/cmn/.htaccess +0 -0
  133. data/template/src/html/images/tmp/.htaccess +0 -0
  134. data/template/src/html/includes/debug.inc +5 -0
  135. data/template/src/html/includes/footer.inc +52 -0
  136. data/template/src/html/includes/header.inc +61 -0
  137. data/template/src/html/includes/html.inc +3 -0
  138. data/template/src/html/includes/namespace.inc +19 -0
  139. data/template/src/html/includes/page.inc +151 -0
  140. data/template/src/html/index.html +35 -0
  141. data/template/src/html/js/cmn/bootstrap.js +74 -0
  142. data/template/src/html/js/cmn/global.js +142 -0
  143. data/template/src/html/js/cmn/lib/LAB.js +348 -0
  144. data/template/src/html/min/.htaccess +4 -0
  145. data/template/src/html/min/MinifyCLI.php +19 -0
  146. data/template/src/html/min/README.txt +132 -0
  147. data/template/src/html/min/builder/_index.js +242 -0
  148. data/template/src/html/min/builder/bm.js +36 -0
  149. data/template/src/html/min/builder/index.php +182 -0
  150. data/template/src/html/min/builder/ocCheck.php +36 -0
  151. data/template/src/html/min/builder/rewriteTest.js +1 -0
  152. data/template/src/html/min/config.php +187 -0
  153. data/template/src/html/min/groupsConfig.php +34 -0
  154. data/template/src/html/min/index.php +66 -0
  155. data/template/src/html/min/lib/FirePHP.php +1370 -0
  156. data/template/src/html/min/lib/HTTP/ConditionalGet.php +348 -0
  157. data/template/src/html/min/lib/HTTP/Encoder.php +326 -0
  158. data/template/src/html/min/lib/JSMin.php +314 -0
  159. data/template/src/html/min/lib/JSMinPlus.php +1872 -0
  160. data/template/src/html/min/lib/Minify.php +532 -0
  161. data/template/src/html/min/lib/Minify/Build.php +103 -0
  162. data/template/src/html/min/lib/Minify/CSS.php +83 -0
  163. data/template/src/html/min/lib/Minify/CSS/Compressor.php +250 -0
  164. data/template/src/html/min/lib/Minify/CSS/UriRewriter.php +270 -0
  165. data/template/src/html/min/lib/Minify/Cache/APC.php +130 -0
  166. data/template/src/html/min/lib/Minify/Cache/File.php +125 -0
  167. data/template/src/html/min/lib/Minify/Cache/Memcache.php +137 -0
  168. data/template/src/html/min/lib/Minify/ClosureCompiler.php +85 -0
  169. data/template/src/html/min/lib/Minify/CommentPreserver.php +90 -0
  170. data/template/src/html/min/lib/Minify/Controller/Base.php +202 -0
  171. data/template/src/html/min/lib/Minify/Controller/Files.php +78 -0
  172. data/template/src/html/min/lib/Minify/Controller/Groups.php +94 -0
  173. data/template/src/html/min/lib/Minify/Controller/MinApp.php +132 -0
  174. data/template/src/html/min/lib/Minify/Controller/Page.php +82 -0
  175. data/template/src/html/min/lib/Minify/Controller/Version1.php +118 -0
  176. data/template/src/html/min/lib/Minify/HTML.php +245 -0
  177. data/template/src/html/min/lib/Minify/ImportProcessor.php +157 -0
  178. data/template/src/html/min/lib/Minify/Lines.php +131 -0
  179. data/template/src/html/min/lib/Minify/Logger.php +45 -0
  180. data/template/src/html/min/lib/Minify/Packer.php +37 -0
  181. data/template/src/html/min/lib/Minify/Source.php +187 -0
  182. data/template/src/html/min/lib/Minify/YUICompressor.php +139 -0
  183. data/template/src/html/min/lib/Solar/Dir.php +199 -0
  184. data/template/src/html/min/lib/closure-compiler.jar +0 -0
  185. data/template/src/html/min/lib/yuicompressor-2.4.2.jar +0 -0
  186. data/template/src/html/min/utils.php +90 -0
  187. data/template/src/templates/css/template.css +7 -0
  188. data/template/src/templates/js/template.js +72 -0
  189. data/template/src/templates/template.html +18 -0
  190. data/template/src/yaml/config.yml +46 -0
  191. data/template/src/yaml/deploy.yml +35 -0
  192. data/test/bixbite_test.rb +7 -0
  193. data/test/test_helper.rb +10 -0
  194. metadata +278 -0
@@ -0,0 +1,219 @@
1
+ ###############################################################################
2
+ #
3
+ # Class: NaturalDocs::Languages::Tcl
4
+ #
5
+ ###############################################################################
6
+ #
7
+ # A subclass to handle the language variations of Tcl.
8
+ #
9
+ ###############################################################################
10
+
11
+ # This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
12
+ # Natural Docs is licensed under the GPL
13
+
14
+ use strict;
15
+ use integer;
16
+
17
+ package NaturalDocs::Languages::Tcl;
18
+
19
+ use base 'NaturalDocs::Languages::Simple';
20
+
21
+
22
+ #
23
+ # bool: pastFirstBrace
24
+ #
25
+ # Whether we've past the first brace in a function prototype or not.
26
+ #
27
+ my $pastFirstBrace;
28
+
29
+
30
+ #
31
+ # Function: OnCode
32
+ #
33
+ # This is just overridden to reset <pastFirstBrace>.
34
+ #
35
+ sub OnCode #(...)
36
+ {
37
+ my ($self, @params) = @_;
38
+
39
+ $pastFirstBrace = 0;
40
+
41
+ return $self->SUPER::OnCode(@params);
42
+ };
43
+
44
+
45
+ #
46
+ # Function: OnPrototypeEnd
47
+ #
48
+ # Tcl's function syntax is shown below.
49
+ #
50
+ # > proc [name] { [params] } { [code] }
51
+ #
52
+ # The opening brace is one of the prototype enders. We need to allow the first opening brace because it contains the
53
+ # parameters.
54
+ #
55
+ # Also, the parameters may have braces within them. I've seen one that used { seconds 20 } as a parameter.
56
+ #
57
+ # Parameters:
58
+ #
59
+ # type - The <TopicType> of the prototype.
60
+ # prototypeRef - A reference to the prototype so far, minus the ender in dispute.
61
+ # ender - The ender symbol.
62
+ #
63
+ # Returns:
64
+ #
65
+ # ENDER_ACCEPT - The ender is accepted and the prototype is finished.
66
+ # ENDER_IGNORE - The ender is rejected and parsing should continue. Note that the prototype will be rejected as a whole
67
+ # if all enders are ignored before reaching the end of the code.
68
+ # ENDER_ACCEPT_AND_CONTINUE - The ender is accepted so the prototype may stand as is. However, the prototype might
69
+ # also continue on so continue parsing. If there is no accepted ender between here and
70
+ # the end of the code this version will be accepted instead.
71
+ # ENDER_REVERT_TO_ACCEPTED - The expedition from ENDER_ACCEPT_AND_CONTINUE failed. Use the last accepted
72
+ # version and end parsing.
73
+ #
74
+ sub OnPrototypeEnd #(type, prototypeRef, ender)
75
+ {
76
+ my ($self, $type, $prototypeRef, $ender) = @_;
77
+
78
+ if ($type eq ::TOPIC_FUNCTION() && $ender eq '{' && !$pastFirstBrace)
79
+ {
80
+ $pastFirstBrace = 1;
81
+ return ::ENDER_IGNORE();
82
+ }
83
+ else
84
+ { return ::ENDER_ACCEPT(); };
85
+ };
86
+
87
+
88
+ #
89
+ # Function: ParsePrototype
90
+ #
91
+ # Parses the prototype and returns it as a <NaturalDocs::Languages::Prototype> object.
92
+ #
93
+ # Parameters:
94
+ #
95
+ # type - The <TopicType>.
96
+ # prototype - The text prototype.
97
+ #
98
+ # Returns:
99
+ #
100
+ # A <NaturalDocs::Languages::Prototype> object.
101
+ #
102
+ sub ParsePrototype #(type, prototype)
103
+ {
104
+ my ($self, $type, $prototype) = @_;
105
+
106
+ if ($type ne ::TOPIC_FUNCTION())
107
+ {
108
+ my $object = NaturalDocs::Languages::Prototype->New($prototype);
109
+ return $object;
110
+ };
111
+
112
+
113
+ # Parse the parameters out of the prototype.
114
+
115
+ my @tokens = $prototype =~ /([^\{\}\ ]+|.)/g;
116
+
117
+ my $parameter;
118
+ my @parameterLines;
119
+
120
+ my $braceLevel = 0;
121
+
122
+ my ($beforeParameters, $afterParameters, $finishedParameters);
123
+
124
+ foreach my $token (@tokens)
125
+ {
126
+ if ($finishedParameters)
127
+ { $afterParameters .= $token; }
128
+
129
+ elsif ($token eq '{')
130
+ {
131
+ if ($braceLevel == 0)
132
+ { $beforeParameters .= $token; }
133
+
134
+ else # braceLevel > 0
135
+ { $parameter .= $token; };
136
+
137
+ $braceLevel++;
138
+ }
139
+
140
+ elsif ($token eq '}')
141
+ {
142
+ if ($braceLevel == 1)
143
+ {
144
+ if ($parameter && $parameter ne ' ')
145
+ { push @parameterLines, $parameter; };
146
+
147
+ $finishedParameters = 1;
148
+ $afterParameters .= $token;
149
+
150
+ $braceLevel--;
151
+ }
152
+ elsif ($braceLevel > 1)
153
+ {
154
+ $parameter .= $token;
155
+ $braceLevel--;
156
+ };
157
+ }
158
+
159
+ elsif ($token eq ' ')
160
+ {
161
+ if ($braceLevel == 1)
162
+ {
163
+ if ($parameter)
164
+ { push @parameterLines, $parameter; };
165
+
166
+ $parameter = undef;
167
+ }
168
+ elsif ($braceLevel > 1)
169
+ {
170
+ $parameter .= $token;
171
+ }
172
+ else
173
+ {
174
+ $beforeParameters .= $token;
175
+ };
176
+ }
177
+
178
+ else
179
+ {
180
+ if ($braceLevel > 0)
181
+ { $parameter .= $token; }
182
+ else
183
+ { $beforeParameters .= $token; };
184
+ };
185
+ };
186
+
187
+ foreach my $part (\$beforeParameters, \$afterParameters)
188
+ {
189
+ $$part =~ s/^ //;
190
+ $$part =~ s/ $//;
191
+ };
192
+
193
+ my $prototypeObject = NaturalDocs::Languages::Prototype->New($beforeParameters, $afterParameters);
194
+
195
+
196
+ # Parse the actual parameters.
197
+
198
+ foreach my $parameterLine (@parameterLines)
199
+ {
200
+ $prototypeObject->AddParameter( $self->ParseParameterLine($parameterLine) );
201
+ };
202
+
203
+ return $prototypeObject;
204
+ };
205
+
206
+
207
+ #
208
+ # Function: ParseParameterLine
209
+ #
210
+ # Parses a prototype parameter line and returns it as a <NaturalDocs::Languages::Prototype::Parameter> object.
211
+ #
212
+ sub ParseParameterLine #(line)
213
+ {
214
+ my ($self, $line) = @_;
215
+ return NaturalDocs::Languages::Prototype::Parameter->New(undef, undef, $line, undef, undef, undef);
216
+ };
217
+
218
+
219
+ 1;
@@ -0,0 +1,3406 @@
1
+ ###############################################################################
2
+ #
3
+ # Package: NaturalDocs::Menu
4
+ #
5
+ ###############################################################################
6
+ #
7
+ # A package handling the menu's contents and state.
8
+ #
9
+ # Usage and Dependencies:
10
+ #
11
+ # - The <Event Handlers> can be called by <NaturalDocs::Project> immediately.
12
+ #
13
+ # - Prior to initialization, <NaturalDocs::Project> must be initialized, and all files that have been changed must be run
14
+ # through <NaturalDocs::Parser->ParseForInformation()>.
15
+ #
16
+ # - To initialize, call <LoadAndUpdate()>. Afterwards, all other functions are available. Also, <LoadAndUpdate()> will
17
+ # call <NaturalDocs::Settings->GenerateDirectoryNames()>.
18
+ #
19
+ # - To save the changes back to disk, call <Save()>.
20
+ #
21
+ ###############################################################################
22
+
23
+ # This file is part of Natural Docs, which is Copyright (C) 2003-2008 Greg Valure
24
+ # Natural Docs is licensed under the GPL
25
+
26
+ use Tie::RefHash;
27
+
28
+ use NaturalDocs::Menu::Entry;
29
+
30
+ use strict;
31
+ use integer;
32
+
33
+ package NaturalDocs::Menu;
34
+
35
+
36
+ #
37
+ # Constants: Constants
38
+ #
39
+ # MAXFILESINGROUP - The maximum number of file entries that can be present in a group before it becomes a candidate for
40
+ # sub-grouping.
41
+ # MINFILESINNEWGROUP - The minimum number of file entries that must be present in a group before it will be automatically
42
+ # created. This is *not* the number of files that must be in a group before it's deleted.
43
+ #
44
+ use constant MAXFILESINGROUP => 6;
45
+ use constant MINFILESINNEWGROUP => 3;
46
+
47
+
48
+ ###############################################################################
49
+ # Group: Variables
50
+
51
+
52
+ #
53
+ # bool: hasChanged
54
+ #
55
+ # Whether the menu changed or not, regardless of why.
56
+ #
57
+ my $hasChanged;
58
+
59
+
60
+ #
61
+ # Object: menu
62
+ #
63
+ # The parsed menu file. Is stored as a <MENU_GROUP> <NaturalDocs::Menu::Entry> object, with the top-level entries being
64
+ # stored as the group's content. This is done because it makes a number of functions simpler to implement, plus it allows group
65
+ # flags to be set on the top-level. However, it is exposed externally via <Content()> as an arrayref.
66
+ #
67
+ # This structure will only contain objects for <MENU_FILE>, <MENU_GROUP>, <MENU_TEXT>, <MENU_LINK>, and
68
+ # <MENU_INDEX> entries. Other types, such as <MENU_TITLE>, are stored in variables such as <title>.
69
+ #
70
+ my $menu;
71
+
72
+ #
73
+ # hash: defaultTitlesChanged
74
+ #
75
+ # An existence hash of default titles that have changed, since <OnDefaultTitleChange()> will be called before
76
+ # <LoadAndUpdate()>. Collects them to be applied later. The keys are the <FileNames>.
77
+ #
78
+ my %defaultTitlesChanged;
79
+
80
+ #
81
+ # String: title
82
+ #
83
+ # The title of the menu.
84
+ #
85
+ my $title;
86
+
87
+ #
88
+ # String: subTitle
89
+ #
90
+ # The sub-title of the menu.
91
+ #
92
+ my $subTitle;
93
+
94
+ #
95
+ # String: footer
96
+ #
97
+ # The footer for the documentation.
98
+ #
99
+ my $footer;
100
+
101
+ #
102
+ # String: timestampText
103
+ #
104
+ # The timestamp for the documentation, stored as the final output text.
105
+ #
106
+ my $timestampText;
107
+
108
+ #
109
+ # String: timestampCode
110
+ #
111
+ # The timestamp for the documentation, storted as the symbolic code.
112
+ #
113
+ my $timestampCode;
114
+
115
+ #
116
+ # hash: indexes
117
+ #
118
+ # An existence hash of all the defined index <TopicTypes> appearing in the menu.
119
+ #
120
+ my %indexes;
121
+
122
+ #
123
+ # hash: previousIndexes
124
+ #
125
+ # An existence hash of all the index <TopicTypes> that appeared in the menu last time.
126
+ #
127
+ my %previousIndexes;
128
+
129
+ #
130
+ # hash: bannedIndexes
131
+ #
132
+ # An existence hash of all the index <TopicTypes> that the user has manually deleted, and thus should not be added back to
133
+ # the menu automatically.
134
+ #
135
+ my %bannedIndexes;
136
+
137
+
138
+ ###############################################################################
139
+ # Group: Files
140
+
141
+ #
142
+ # File: Menu.txt
143
+ #
144
+ # The file used to generate the menu.
145
+ #
146
+ # Format:
147
+ #
148
+ # The file is plain text. Blank lines can appear anywhere and are ignored. Tags and their content must be completely
149
+ # contained on one line with the exception of Group's braces. All values in brackets below are encoded with entity characters.
150
+ #
151
+ # > # [comment]
152
+ #
153
+ # The file supports single-line comments via #. They can appear alone on a line or after content.
154
+ #
155
+ # > Format: [version]
156
+ # > Title: [title]
157
+ # > SubTitle: [subtitle]
158
+ # > Footer: [footer]
159
+ # > Timestamp: [timestamp code]
160
+ #
161
+ # The file format version, menu title, subtitle, footer, and timestamp are specified as above. Each can only be specified once,
162
+ # with subsequent ones being ignored. Subtitle is ignored if Title is not present. Format must be the first entry in the file. If
163
+ # it's not present, it's assumed the menu is from version 0.95 or earlier, since it was added with 1.0.
164
+ #
165
+ # The timestamp code is as follows.
166
+ #
167
+ # m - Single digit month, where applicable. January is "1".
168
+ # mm - Always double digit month. January is "01".
169
+ # mon - Short month word. January is "Jan".
170
+ # month - Long month word. January is "January".
171
+ # d - Single digit day, where applicable. 1 is "1".
172
+ # dd - Always double digit day. 1 is "01".
173
+ # day - Day with text extension. 1 is "1st".
174
+ # yy - Double digit year. 2006 is "06".
175
+ # yyyy - Four digit year. 2006 is "2006".
176
+ # year - Four digit year. 2006 is "2006".
177
+ #
178
+ # Anything else is left literal in the output.
179
+ #
180
+ # > File: [title] ([file name])
181
+ # > File: [title] (auto-title, [file name])
182
+ # > File: [title] (no auto-title, [file name])
183
+ #
184
+ # Files are specified as above. If there is only one input directory, file names are relative. Otherwise they are absolute.
185
+ # If "no auto-title" is specified, the title on the line is used. If not, the title is ignored and the
186
+ # default file title is used instead. Auto-title defaults to on, so specifying "auto-title" is for compatibility only.
187
+ #
188
+ # > Group: [title]
189
+ # > Group: [title] { ... }
190
+ #
191
+ # Groups are specified as above. If no braces are specified, the group's content is everything that follows until the end of the
192
+ # file, the next group (braced or unbraced), or the closing brace of a parent group. Group braces are the only things in this
193
+ # file that can span multiple lines.
194
+ #
195
+ # There is no limitations on where the braces can appear. The opening brace can appear after the group tag, on its own line,
196
+ # or preceding another tag on a line. Similarly, the closing brace can appear after another tag or on its own line. Being
197
+ # bitchy here would just get in the way of quick and dirty editing; the package will clean it up automatically when it writes it
198
+ # back to disk.
199
+ #
200
+ # > Text: [text]
201
+ #
202
+ # Arbitrary text is specified as above. As with other tags, everything must be contained on the same line.
203
+ #
204
+ # > Link: [URL]
205
+ # > Link: [title] ([URL])
206
+ #
207
+ # External links can be specified as above. If the titled form is not used, the URL is used as the title.
208
+ #
209
+ # > Index: [name]
210
+ # > [topic type name] Index: [name]
211
+ #
212
+ # Indexes are specified as above. The topic type names can be either singular or plural. General is assumed if not specified.
213
+ #
214
+ # > Don't Index: [topic type name]
215
+ # > Don't Index: [topic type name], [topic type name], ...
216
+ #
217
+ # The option above prevents indexes that exist but are not on the menu from being automatically added.
218
+ #
219
+ # > Data: [number]([obscured data])
220
+ #
221
+ # Used to store non-user editable data.
222
+ #
223
+ # > Data: 1([obscured: [directory name]///[input directory]])
224
+ #
225
+ # When there is more than one directory, these lines store the input directories used in the last run and their names. This
226
+ # allows menu files to be shared across machines since the names will be consistent and the directories can be used to convert
227
+ # filenames to the local machine's paths. We don't want this user-editable because they may think changing it changes the
228
+ # input directories, when it doesn't. Also, changing it without changing all the paths screws up resolving.
229
+ #
230
+ # > Data: 2([obscured: [directory name])
231
+ #
232
+ # When there is only one directory and its name is not "default", this stores the name.
233
+ #
234
+ #
235
+ # Entities:
236
+ #
237
+ # &amp; - Ampersand.
238
+ # &lparen; - Left parenthesis.
239
+ # &rparen; - Right parenthesis.
240
+ # &lbrace; - Left brace.
241
+ # &rbrace; - Right brace.
242
+ #
243
+ #
244
+ # Revisions:
245
+ #
246
+ # 1.4:
247
+ #
248
+ # - Added Timestamp property.
249
+ # - Values are now encoded with entity characters.
250
+ #
251
+ # 1.3:
252
+ #
253
+ # - File names are now relative again if there is only one input directory.
254
+ # - Data: 2(...) added.
255
+ # - Can't use synonyms like "copyright" for "footer" or "sub-title" for "subtitle".
256
+ # - "Don't Index" line now requires commas to separate them, whereas it tolerated just spaces before.
257
+ #
258
+ # 1.16:
259
+ #
260
+ # - File names are now absolute instead of relative. Prior to 1.16 only one input directory was allowed, so they could be
261
+ # relative.
262
+ # - Data keywords introduced to store input directories and their names.
263
+ #
264
+ # 1.14:
265
+ #
266
+ # - Renamed this file from NaturalDocs_Menu.txt to Menu.txt.
267
+ #
268
+ # 1.1:
269
+ #
270
+ # - Added the "don't index" line.
271
+ #
272
+ # This is also the point where indexes were automatically added and removed, so all index entries from prior revisions
273
+ # were manually added and are not guaranteed to contain anything.
274
+ #
275
+ # 1.0:
276
+ #
277
+ # - Added the format line.
278
+ # - Added the "no auto-title" attribute.
279
+ # - Changed the file entry default to auto-title.
280
+ #
281
+ # This is also the point where auto-organization and better auto-titles were introduced. All groups in prior revisions were
282
+ # manually added, with the exception of a top-level Other group where new files were automatically added if there were
283
+ # groups defined.
284
+ #
285
+ # Break in support:
286
+ #
287
+ # Releases prior to 1.0 are no longer supported. Why?
288
+ #
289
+ # - They don't have a Format: line, which is required by <NaturalDocs::ConfigFile>, although I could work around this
290
+ # if I needed to.
291
+ # - No significant number of downloads for pre-1.0 releases.
292
+ # - Code simplification. I don't have to bridge the conversion from manual-only menu organization to automatic.
293
+ #
294
+ # 0.9:
295
+ #
296
+ # - Added index entries.
297
+ #
298
+
299
+ #
300
+ # File: PreviousMenuState.nd
301
+ #
302
+ # The file used to store the previous state of the menu so as to detect changes.
303
+ #
304
+ #
305
+ # Format:
306
+ #
307
+ # > [BINARY_FORMAT]
308
+ # > [VersionInt: app version]
309
+ #
310
+ # First is the standard <BINARY_FORMAT> <VersionInt> header.
311
+ #
312
+ # > [UInt8: 0 (end group)]
313
+ # > [UInt8: MENU_FILE] [UInt8: noAutoTitle] [AString16: title] [AString16: target]
314
+ # > [UInt8: MENU_GROUP] [AString16: title]
315
+ # > [UInt8: MENU_INDEX] [AString16: title] [AString16: topic type]
316
+ # > [UInt8: MENU_LINK] [AString16: title] [AString16: url]
317
+ # > [UInt8: MENU_TEXT] [AString16: text]
318
+ #
319
+ # The first UInt8 of each following line is either zero or one of the <Menu Entry Types>. What follows is contextual.
320
+ #
321
+ # There are no entries for title, subtitle, or footer. Only the entries present in <menu>.
322
+ #
323
+ # See Also:
324
+ #
325
+ # <File Format Conventions>
326
+ #
327
+ # Dependencies:
328
+ #
329
+ # - Because the type is represented by a UInt8, the <Menu Entry Types> must all be <= 255.
330
+ #
331
+ # Revisions:
332
+ #
333
+ # 1.3:
334
+ #
335
+ # - The topic type following the <MENU_INDEX> entries were changed from UInt8s to AString16s, since <TopicTypes>
336
+ # were switched from integer constants to strings. You can still convert the old to the new via
337
+ # <NaturalDocs::Topics->TypeFromLegacy()>.
338
+ #
339
+ # 1.16:
340
+ #
341
+ # - The file targets are now absolute. Prior to 1.16, they were relative to the input directory since only one was allowed.
342
+ #
343
+ # 1.14:
344
+ #
345
+ # - The file was renamed from NaturalDocs.m to PreviousMenuState.nd and moved into the Data subdirectory.
346
+ #
347
+ # 1.0:
348
+ #
349
+ # - The file's format was completely redone. Prior to 1.0, the file was a text file consisting of the app version and a line
350
+ # which was a tab-separated list of the indexes present in the menu. * meant the general index.
351
+ #
352
+ # Break in support:
353
+ #
354
+ # Pre-1.0 files are no longer supported. There was no significant number of downloads for pre-1.0 releases, and this
355
+ # eliminates a separate code path for them.
356
+ #
357
+ # 0.95:
358
+ #
359
+ # - Change the file version to match the app version. Prior to 0.95, the version line was 1. Test for "1" instead of "1.0" to
360
+ # distinguish.
361
+ #
362
+ # 0.9:
363
+ #
364
+ # - The file was added to the project. Prior to 0.9, it didn't exist.
365
+ #
366
+
367
+
368
+ ###############################################################################
369
+ # Group: File Functions
370
+
371
+ #
372
+ # Function: LoadAndUpdate
373
+ #
374
+ # Loads the menu file from disk and updates it. Will add, remove, rearrange, and remove auto-titling from entries as
375
+ # necessary. Will also call <NaturalDocs::Settings->GenerateDirectoryNames()>.
376
+ #
377
+ sub LoadAndUpdate
378
+ {
379
+ my ($self) = @_;
380
+
381
+ my ($inputDirectoryNames, $relativeFiles, $onlyDirectoryName) = $self->LoadMenuFile();
382
+
383
+ my $errorCount = NaturalDocs::ConfigFile->ErrorCount();
384
+ if ($errorCount)
385
+ {
386
+ NaturalDocs::ConfigFile->PrintErrorsAndAnnotateFile();
387
+ NaturalDocs::Error->SoftDeath('There ' . ($errorCount == 1 ? 'is an error' : 'are ' . $errorCount . ' errors')
388
+ . ' in ' . NaturalDocs::Project->UserConfigFile('Menu.txt'));
389
+ };
390
+
391
+ # If the menu has a timestamp and today is a different day than the last time Natural Docs was run, we have to count it as the
392
+ # menu changing.
393
+ if (defined $timestampCode)
394
+ {
395
+ my (undef, undef, undef, $currentDay, $currentMonth, $currentYear) = localtime();
396
+ my (undef, undef, undef, $lastDay, $lastMonth, $lastYear) =
397
+ localtime( (stat( NaturalDocs::Project->DataFile('PreviousMenuState.nd') ))[9] );
398
+ # This should be okay if the previous menu state file doesn't exist.
399
+
400
+ if ($currentDay != $lastDay || $currentMonth != $lastMonth || $currentYear != $lastYear)
401
+ { $hasChanged = 1; };
402
+ };
403
+
404
+
405
+ if ($relativeFiles)
406
+ {
407
+ my $inputDirectory = $self->ResolveRelativeInputDirectories($onlyDirectoryName);
408
+
409
+ if ($onlyDirectoryName)
410
+ { $inputDirectoryNames = { $inputDirectory => $onlyDirectoryName }; };
411
+ }
412
+ else
413
+ { $self->ResolveInputDirectories($inputDirectoryNames); };
414
+
415
+ NaturalDocs::Settings->GenerateDirectoryNames($inputDirectoryNames);
416
+
417
+ my $filesInMenu = $self->FilesInMenu();
418
+
419
+ my ($previousMenu, $previousIndexes, $previousFiles) = $self->LoadPreviousMenuStateFile();
420
+
421
+ if (defined $previousIndexes)
422
+ { %previousIndexes = %$previousIndexes; };
423
+
424
+ if (defined $previousFiles)
425
+ { $self->LockUserTitleChanges($previousFiles); };
426
+
427
+ # Don't need these anymore. We keep this level of detail because it may be used more in the future.
428
+ $previousMenu = undef;
429
+ $previousFiles = undef;
430
+ $previousIndexes = undef;
431
+
432
+ # We flag title changes instead of actually performing them at this point for two reasons. First, contents of groups are still
433
+ # subject to change, which would affect the generated titles. Second, we haven't detected the sort order yet. Changing titles
434
+ # could make groups appear unalphabetized when they were beforehand.
435
+
436
+ my $updateAllTitles;
437
+
438
+ # If the menu file changed, we can't be sure which groups changed and which didn't without a comparison, which really isn't
439
+ # worth the trouble. So we regenerate all the titles instead.
440
+ if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') == ::FILE_CHANGED())
441
+ { $updateAllTitles = 1; }
442
+ else
443
+ { $self->FlagAutoTitleChanges(); };
444
+
445
+ # We add new files before deleting old files so their presence still affects the grouping. If we deleted old files first, it could
446
+ # throw off where to place the new ones.
447
+
448
+ $self->AutoPlaceNewFiles($filesInMenu);
449
+
450
+ my $numberRemoved = $self->RemoveDeadFiles();
451
+
452
+ $self->CheckForTrashedMenu(scalar keys %$filesInMenu, $numberRemoved);
453
+
454
+ # Don't ban indexes if they deleted Menu.txt. They may have not deleted PreviousMenuState.nd and we don't want everything
455
+ # to be banned because of it.
456
+ if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') != ::FILE_DOESNTEXIST())
457
+ { $self->BanAndUnbanIndexes(); };
458
+
459
+ # Index groups need to be detected before adding new ones.
460
+
461
+ $self->DetectIndexGroups();
462
+
463
+ $self->AddAndRemoveIndexes();
464
+
465
+ # We wait until after new files are placed to remove dead groups because a new file may save a group.
466
+
467
+ $self->RemoveDeadGroups();
468
+
469
+ $self->CreateDirectorySubGroups();
470
+
471
+ # We detect the sort before regenerating the titles so it doesn't get thrown off by changes. However, we do it after deleting
472
+ # dead entries and moving things into subgroups because their removal may bump it into a stronger sort category (i.e.
473
+ # SORTFILESANDGROUPS instead of just SORTFILES.) New additions don't factor into the sort.
474
+
475
+ $self->DetectOrder($updateAllTitles);
476
+
477
+ $self->GenerateAutoFileTitles($updateAllTitles);
478
+
479
+ $self->ResortGroups($updateAllTitles);
480
+
481
+
482
+ # Don't need this anymore.
483
+ %defaultTitlesChanged = ( );
484
+ };
485
+
486
+
487
+ #
488
+ # Function: Save
489
+ #
490
+ # Writes the changes to the menu files.
491
+ #
492
+ sub Save
493
+ {
494
+ my ($self) = @_;
495
+
496
+ if ($hasChanged)
497
+ {
498
+ $self->SaveMenuFile();
499
+ $self->SavePreviousMenuStateFile();
500
+ };
501
+ };
502
+
503
+
504
+ ###############################################################################
505
+ # Group: Information Functions
506
+
507
+ #
508
+ # Function: HasChanged
509
+ #
510
+ # Returns whether the menu has changed or not.
511
+ #
512
+ sub HasChanged
513
+ { return $hasChanged; };
514
+
515
+ #
516
+ # Function: Content
517
+ #
518
+ # Returns the parsed menu as an arrayref of <NaturalDocs::Menu::Entry> objects. Do not change the arrayref.
519
+ #
520
+ # The arrayref will only contain <MENU_FILE>, <MENU_GROUP>, <MENU_INDEX>, <MENU_TEXT>, and <MENU_LINK>
521
+ # entries. Entries such as <MENU_TITLE> are parsed out and are only accessible via functions such as <Title()>.
522
+ #
523
+ sub Content
524
+ { return $menu->GroupContent(); };
525
+
526
+ #
527
+ # Function: Title
528
+ #
529
+ # Returns the title of the menu, or undef if none.
530
+ #
531
+ sub Title
532
+ { return $title; };
533
+
534
+ #
535
+ # Function: SubTitle
536
+ #
537
+ # Returns the sub-title of the menu, or undef if none.
538
+ #
539
+ sub SubTitle
540
+ { return $subTitle; };
541
+
542
+ #
543
+ # Function: Footer
544
+ #
545
+ # Returns the footer of the documentation, or undef if none.
546
+ #
547
+ sub Footer
548
+ { return $footer; };
549
+
550
+ #
551
+ # Function: TimeStamp
552
+ #
553
+ # Returns the timestamp text of the documentation, or undef if none.
554
+ #
555
+ sub TimeStamp
556
+ { return $timestampText; };
557
+
558
+ #
559
+ # Function: Indexes
560
+ #
561
+ # Returns an existence hashref of all the index <TopicTypes> appearing in the menu. Do not change the hashref.
562
+ #
563
+ sub Indexes
564
+ { return \%indexes; };
565
+
566
+ #
567
+ # Function: PreviousIndexes
568
+ #
569
+ # Returns an existence hashref of all the index <TopicTypes> that previously appeared in the menu. Do not change the
570
+ # hashref.
571
+ #
572
+ sub PreviousIndexes
573
+ { return \%previousIndexes; };
574
+
575
+
576
+ #
577
+ # Function: FilesInMenu
578
+ #
579
+ # Returns a hashref of all the files present in the menu. The keys are the <FileNames>, and the values are references to their
580
+ # <NaturalDocs::Menu::Entry> objects.
581
+ #
582
+ sub FilesInMenu
583
+ {
584
+ my ($self) = @_;
585
+
586
+ my @groupStack = ( $menu );
587
+ my $filesInMenu = { };
588
+
589
+ while (scalar @groupStack)
590
+ {
591
+ my $currentGroup = pop @groupStack;
592
+ my $currentGroupContent = $currentGroup->GroupContent();
593
+
594
+ foreach my $entry (@$currentGroupContent)
595
+ {
596
+ if ($entry->Type() == ::MENU_GROUP())
597
+ { push @groupStack, $entry; }
598
+ elsif ($entry->Type() == ::MENU_FILE())
599
+ { $filesInMenu->{ $entry->Target() } = $entry; };
600
+ };
601
+ };
602
+
603
+ return $filesInMenu;
604
+ };
605
+
606
+
607
+
608
+ ###############################################################################
609
+ # Group: Event Handlers
610
+ #
611
+ # These functions are called by <NaturalDocs::Project> only. You don't need to worry about calling them. For example, when
612
+ # changing the default menu title of a file, you only need to call <NaturalDocs::Project->SetDefaultMenuTitle()>. That function
613
+ # will handle calling <OnDefaultTitleChange()>.
614
+
615
+
616
+ #
617
+ # Function: OnDefaultTitleChange
618
+ #
619
+ # Called by <NaturalDocs::Project> if the default menu title of a source file has changed.
620
+ #
621
+ # Parameters:
622
+ #
623
+ # file - The source <FileName> that had its default menu title changed.
624
+ #
625
+ sub OnDefaultTitleChange #(file)
626
+ {
627
+ my ($self, $file) = @_;
628
+
629
+ # Collect them for later. We'll deal with them in LoadAndUpdate().
630
+
631
+ $defaultTitlesChanged{$file} = 1;
632
+ };
633
+
634
+
635
+
636
+ ###############################################################################
637
+ # Group: Support Functions
638
+
639
+
640
+ #
641
+ # Function: LoadMenuFile
642
+ #
643
+ # Loads and parses the menu file <Menu.txt>. This will fill <menu>, <title>, <subTitle>, <footer>, <timestampText>,
644
+ # <timestampCode>, <indexes>, and <bannedIndexes>. If there are any errors in the file, they will be recorded with
645
+ # <NaturalDocs::ConfigFile->AddError()>.
646
+ #
647
+ # Returns:
648
+ #
649
+ # The array ( inputDirectories, relativeFiles, onlyDirectoryName ) or an empty array if the file doesn't exist.
650
+ #
651
+ # inputDirectories - A hashref of all the input directories and their names stored in the menu file. The keys are the
652
+ # directories and the values are their names. Undef if none.
653
+ # relativeFiles - Whether the menu uses relative file names.
654
+ # onlyDirectoryName - The name of the input directory if there is only one.
655
+ #
656
+ sub LoadMenuFile
657
+ {
658
+ my ($self) = @_;
659
+
660
+ my $inputDirectories = { };
661
+ my $relativeFiles;
662
+ my $onlyDirectoryName;
663
+
664
+ # A stack of Menu::Entry object references as we move through the groups.
665
+ my @groupStack;
666
+
667
+ $menu = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), undef, undef, undef);
668
+ my $currentGroup = $menu;
669
+
670
+ # Whether we're currently in a braceless group, since we'd have to find the implied end rather than an explicit one.
671
+ my $inBracelessGroup;
672
+
673
+ # Whether we're right after a group token, which is the only place there can be an opening brace.
674
+ my $afterGroupToken;
675
+
676
+ my $version;
677
+
678
+ if ($version = NaturalDocs::ConfigFile->Open(NaturalDocs::Project->UserConfigFile('Menu.txt'), 1))
679
+ {
680
+ # We don't check if the menu file is from a future version because we can't just throw it out and regenerate it like we can
681
+ # with other data files. So we just keep going regardless. Any syntactic differences will show up as errors.
682
+
683
+ while (my ($keyword, $value, $comment) = NaturalDocs::ConfigFile->GetLine())
684
+ {
685
+ # Check for an opening brace after a group token. This has to be separate from the rest of the code because the flag
686
+ # needs to be reset after every line.
687
+ if ($afterGroupToken)
688
+ {
689
+ $afterGroupToken = undef;
690
+
691
+ if ($keyword eq '{')
692
+ {
693
+ $inBracelessGroup = undef;
694
+ next;
695
+ }
696
+ else
697
+ { $inBracelessGroup = 1; };
698
+ };
699
+
700
+
701
+ # Now on to the real code.
702
+
703
+ if ($keyword eq 'file')
704
+ {
705
+ my $flags = 0;
706
+
707
+ if ($value =~ /^(.+)\(([^\(]+)\)$/)
708
+ {
709
+ my ($title, $file) = ($1, $2);
710
+
711
+ $title =~ s/ +$//;
712
+
713
+ # Check for auto-title modifier.
714
+ if ($file =~ /^((?:no )?auto-title, ?)(.+)$/i)
715
+ {
716
+ my $modifier;
717
+ ($modifier, $file) = ($1, $2);
718
+
719
+ if ($modifier =~ /^no/i)
720
+ { $flags |= ::MENU_FILE_NOAUTOTITLE(); };
721
+ };
722
+
723
+ my $entry = NaturalDocs::Menu::Entry->New(::MENU_FILE(), $self->RestoreAmpChars($title),
724
+ $self->RestoreAmpChars($file), $flags);
725
+
726
+ $currentGroup->PushToGroup($entry);
727
+ }
728
+ else
729
+ { NaturalDocs::ConfigFile->AddError('File lines must be in the format "File: [title] ([location])"'); };
730
+ }
731
+
732
+
733
+ elsif ($keyword eq 'group')
734
+ {
735
+ # End a braceless group, if we were in one.
736
+ if ($inBracelessGroup)
737
+ {
738
+ $currentGroup = pop @groupStack;
739
+ $inBracelessGroup = undef;
740
+ };
741
+
742
+ my $entry = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), $self->RestoreAmpChars($value), undef, undef);
743
+
744
+ $currentGroup->PushToGroup($entry);
745
+
746
+ push @groupStack, $currentGroup;
747
+ $currentGroup = $entry;
748
+
749
+ $afterGroupToken = 1;
750
+ }
751
+
752
+
753
+ elsif ($keyword eq '{')
754
+ {
755
+ NaturalDocs::ConfigFile->AddError('Opening braces are only allowed after Group tags.');
756
+ }
757
+
758
+
759
+ elsif ($keyword eq '}')
760
+ {
761
+ # End a braceless group, if we were in one.
762
+ if ($inBracelessGroup)
763
+ {
764
+ $currentGroup = pop @groupStack;
765
+ $inBracelessGroup = undef;
766
+ };
767
+
768
+ # End a braced group too.
769
+ if (scalar @groupStack)
770
+ { $currentGroup = pop @groupStack; }
771
+ else
772
+ { NaturalDocs::ConfigFile->AddError('Unmatched closing brace.'); };
773
+ }
774
+
775
+
776
+ elsif ($keyword eq 'title')
777
+ {
778
+ if (!defined $title)
779
+ { $title = $self->RestoreAmpChars($value); }
780
+ else
781
+ { NaturalDocs::ConfigFile->AddError('Title can only be defined once.'); };
782
+ }
783
+
784
+
785
+ elsif ($keyword eq 'subtitle')
786
+ {
787
+ if (defined $title)
788
+ {
789
+ if (!defined $subTitle)
790
+ { $subTitle = $self->RestoreAmpChars($value); }
791
+ else
792
+ { NaturalDocs::ConfigFile->AddError('SubTitle can only be defined once.'); };
793
+ }
794
+ else
795
+ { NaturalDocs::ConfigFile->AddError('Title must be defined before SubTitle.'); };
796
+ }
797
+
798
+
799
+ elsif ($keyword eq 'footer')
800
+ {
801
+ if (!defined $footer)
802
+ { $footer = $self->RestoreAmpChars($value); }
803
+ else
804
+ { NaturalDocs::ConfigFile->AddError('Footer can only be defined once.'); };
805
+ }
806
+
807
+
808
+ elsif ($keyword eq 'timestamp')
809
+ {
810
+ if (!defined $timestampCode)
811
+ {
812
+ $timestampCode = $self->RestoreAmpChars($value);
813
+ $self->GenerateTimestampText();
814
+ }
815
+ else
816
+ { NaturalDocs::ConfigFile->AddError('Timestamp can only be defined once.'); };
817
+ }
818
+
819
+
820
+ elsif ($keyword eq 'text')
821
+ {
822
+ $currentGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_TEXT(), $self->RestoreAmpChars($value),
823
+ undef, undef) );
824
+ }
825
+
826
+
827
+ elsif ($keyword eq 'link')
828
+ {
829
+ my ($title, $url);
830
+
831
+ if ($value =~ /^([^\(\)]+?) ?\(([^\)]+)\)$/)
832
+ {
833
+ ($title, $url) = ($1, $2);
834
+ }
835
+ elsif (defined $comment)
836
+ {
837
+ $value .= $comment;
838
+
839
+ if ($value =~ /^([^\(\)]+?) ?\(([^\)]+)\) ?(?:#.*)?$/)
840
+ {
841
+ ($title, $url) = ($1, $2);
842
+ };
843
+ };
844
+
845
+ if ($title)
846
+ {
847
+ $currentGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_LINK(), $self->RestoreAmpChars($title),
848
+ $self->RestoreAmpChars($url), undef) );
849
+ }
850
+ else
851
+ { NaturalDocs::ConfigFile->AddError('Link lines must be in the format "Link: [title] ([url])"'); };
852
+ }
853
+
854
+
855
+ elsif ($keyword eq 'data')
856
+ {
857
+ $value =~ /^(\d)\((.*)\)$/;
858
+ my ($number, $data) = ($1, $2);
859
+
860
+ $data = NaturalDocs::ConfigFile->Unobscure($data);
861
+
862
+ # The input directory naming convention changed with version 1.32, but NaturalDocs::Settings will handle that
863
+ # automatically.
864
+
865
+ if ($number == 1)
866
+ {
867
+ my ($dirName, $inputDir) = split(/\/\/\//, $data, 2);
868
+ $inputDirectories->{$inputDir} = $dirName;
869
+ }
870
+ elsif ($number == 2)
871
+ { $onlyDirectoryName = $data; };
872
+ # Ignore other numbers because it may be from a future format and we don't want to make the user delete it
873
+ # manually.
874
+ }
875
+
876
+ elsif ($keyword eq "don't index")
877
+ {
878
+ my @indexes = split(/, ?/, $value);
879
+
880
+ foreach my $index (@indexes)
881
+ {
882
+ my $indexType = NaturalDocs::Topics->TypeFromName( $self->RestoreAmpChars($index) );
883
+
884
+ if (defined $indexType)
885
+ { $bannedIndexes{$indexType} = 1; };
886
+ };
887
+ }
888
+
889
+ elsif ($keyword eq 'index')
890
+ {
891
+ my $entry = NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $self->RestoreAmpChars($value),
892
+ ::TOPIC_GENERAL(), undef);
893
+ $currentGroup->PushToGroup($entry);
894
+
895
+ $indexes{::TOPIC_GENERAL()} = 1;
896
+ }
897
+
898
+ elsif (substr($keyword, -6) eq ' index')
899
+ {
900
+ my $index = substr($keyword, 0, -6);
901
+ my ($indexType, $indexInfo) = NaturalDocs::Topics->NameInfo( $self->RestoreAmpChars($index) );
902
+
903
+ if (defined $indexType)
904
+ {
905
+ if ($indexInfo->Index())
906
+ {
907
+ $indexes{$indexType} = 1;
908
+ $currentGroup->PushToGroup(
909
+ NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $self->RestoreAmpChars($value), $indexType, undef) );
910
+ }
911
+ else
912
+ {
913
+ # If it's on the menu but isn't indexable, the topic setting may have changed out from under it.
914
+ $hasChanged = 1;
915
+ };
916
+ }
917
+ else
918
+ {
919
+ NaturalDocs::ConfigFile->AddError($index . ' is not a valid index type.');
920
+ };
921
+ }
922
+
923
+ else
924
+ {
925
+ NaturalDocs::ConfigFile->AddError(ucfirst($keyword) . ' is not a valid keyword.');
926
+ };
927
+ };
928
+
929
+
930
+ # End a braceless group, if we were in one.
931
+ if ($inBracelessGroup)
932
+ {
933
+ $currentGroup = pop @groupStack;
934
+ $inBracelessGroup = undef;
935
+ };
936
+
937
+ # Close up all open groups.
938
+ my $openGroups = 0;
939
+ while (scalar @groupStack)
940
+ {
941
+ $currentGroup = pop @groupStack;
942
+ $openGroups++;
943
+ };
944
+
945
+ if ($openGroups == 1)
946
+ { NaturalDocs::ConfigFile->AddError('There is an unclosed group.'); }
947
+ elsif ($openGroups > 1)
948
+ { NaturalDocs::ConfigFile->AddError('There are ' . $openGroups . ' unclosed groups.'); };
949
+
950
+
951
+ if (!scalar keys %$inputDirectories)
952
+ {
953
+ $inputDirectories = undef;
954
+ $relativeFiles = 1;
955
+ };
956
+
957
+ NaturalDocs::ConfigFile->Close();
958
+
959
+ return ($inputDirectories, $relativeFiles, $onlyDirectoryName);
960
+ }
961
+
962
+ else
963
+ { return ( ); };
964
+ };
965
+
966
+
967
+ #
968
+ # Function: SaveMenuFile
969
+ #
970
+ # Saves the current menu to <Menu.txt>.
971
+ #
972
+ sub SaveMenuFile
973
+ {
974
+ my ($self) = @_;
975
+
976
+ open(MENUFILEHANDLE, '>' . NaturalDocs::Project->UserConfigFile('Menu.txt'))
977
+ or die "Couldn't save menu file " . NaturalDocs::Project->UserConfigFile('Menu.txt') . "\n";
978
+
979
+
980
+ print MENUFILEHANDLE
981
+ "Format: " . NaturalDocs::Settings->TextAppVersion() . "\n\n\n";
982
+
983
+ my $inputDirs = NaturalDocs::Settings->InputDirectories();
984
+
985
+
986
+ if (defined $title)
987
+ {
988
+ print MENUFILEHANDLE 'Title: ' . $self->ConvertAmpChars($title) . "\n";
989
+
990
+ if (defined $subTitle)
991
+ {
992
+ print MENUFILEHANDLE 'SubTitle: ' . $self->ConvertAmpChars($subTitle) . "\n";
993
+ }
994
+ else
995
+ {
996
+ print MENUFILEHANDLE
997
+ "\n"
998
+ . "# You can also add a sub-title to your menu like this:\n"
999
+ . "# SubTitle: [subtitle]\n";
1000
+ };
1001
+ }
1002
+ else
1003
+ {
1004
+ print MENUFILEHANDLE
1005
+ "# You can add a title and sub-title to your menu like this:\n"
1006
+ . "# Title: [project name]\n"
1007
+ . "# SubTitle: [subtitle]\n";
1008
+ };
1009
+
1010
+ print MENUFILEHANDLE "\n";
1011
+
1012
+ if (defined $footer)
1013
+ {
1014
+ print MENUFILEHANDLE 'Footer: ' . $self->ConvertAmpChars($footer) . "\n";
1015
+ }
1016
+ else
1017
+ {
1018
+ print MENUFILEHANDLE
1019
+ "# You can add a footer to your documentation like this:\n"
1020
+ . "# Footer: [text]\n"
1021
+ . "# If you want to add a copyright notice, this would be the place to do it.\n";
1022
+ };
1023
+
1024
+ if (defined $timestampCode)
1025
+ {
1026
+ print MENUFILEHANDLE 'Timestamp: ' . $self->ConvertAmpChars($timestampCode) . "\n";
1027
+ }
1028
+ else
1029
+ {
1030
+ print MENUFILEHANDLE
1031
+ "\n"
1032
+ . "# You can add a timestamp to your documentation like one of these:\n"
1033
+ . "# Timestamp: Generated on month day, year\n"
1034
+ . "# Timestamp: Updated mm/dd/yyyy\n"
1035
+ . "# Timestamp: Last updated mon day\n"
1036
+ . "#\n";
1037
+ };
1038
+
1039
+ print MENUFILEHANDLE
1040
+ qq{# m - One or two digit month. January is "1"\n}
1041
+ . qq{# mm - Always two digit month. January is "01"\n}
1042
+ . qq{# mon - Short month word. January is "Jan"\n}
1043
+ . qq{# month - Long month word. January is "January"\n}
1044
+ . qq{# d - One or two digit day. 1 is "1"\n}
1045
+ . qq{# dd - Always two digit day. 1 is "01"\n}
1046
+ . qq{# day - Day with letter extension. 1 is "1st"\n}
1047
+ . qq{# yy - Two digit year. 2006 is "06"\n}
1048
+ . qq{# yyyy - Four digit year. 2006 is "2006"\n}
1049
+ . qq{# year - Four digit year. 2006 is "2006"\n}
1050
+
1051
+ . "\n";
1052
+
1053
+ if (scalar keys %bannedIndexes)
1054
+ {
1055
+ print MENUFILEHANDLE
1056
+
1057
+ "# These are indexes you deleted, so Natural Docs will not add them again\n"
1058
+ . "# unless you remove them from this line.\n"
1059
+ . "\n"
1060
+ . "Don't Index: ";
1061
+
1062
+ my $first = 1;
1063
+
1064
+ foreach my $index (keys %bannedIndexes)
1065
+ {
1066
+ if (!$first)
1067
+ { print MENUFILEHANDLE ', '; }
1068
+ else
1069
+ { $first = undef; };
1070
+
1071
+ print MENUFILEHANDLE $self->ConvertAmpChars( NaturalDocs::Topics->NameOfType($index, 1), CONVERT_COMMAS() );
1072
+ };
1073
+
1074
+ print MENUFILEHANDLE "\n\n";
1075
+ };
1076
+
1077
+
1078
+ # Remember to keep lines below eighty characters.
1079
+
1080
+ print MENUFILEHANDLE
1081
+ "\n"
1082
+ . "# --------------------------------------------------------------------------\n"
1083
+ . "# \n"
1084
+ . "# Cut and paste the lines below to change the order in which your files\n"
1085
+ . "# appear on the menu. Don't worry about adding or removing files, Natural\n"
1086
+ . "# Docs will take care of that.\n"
1087
+ . "# \n"
1088
+ . "# You can further organize the menu by grouping the entries. Add a\n"
1089
+ . "# \"Group: [name] {\" line to start a group, and add a \"}\" to end it.\n"
1090
+ . "# \n"
1091
+ . "# You can add text and web links to the menu by adding \"Text: [text]\" and\n"
1092
+ . "# \"Link: [name] ([URL])\" lines, respectively.\n"
1093
+ . "# \n"
1094
+ . "# The formatting and comments are auto-generated, so don't worry about\n"
1095
+ . "# neatness when editing the file. Natural Docs will clean it up the next\n"
1096
+ . "# time it is run. When working with groups, just deal with the braces and\n"
1097
+ . "# forget about the indentation and comments.\n"
1098
+ . "# \n";
1099
+
1100
+ if (scalar @$inputDirs > 1)
1101
+ {
1102
+ print MENUFILEHANDLE
1103
+ "# You can use this file on other computers even if they use different\n"
1104
+ . "# directories. As long as the command line points to the same source files,\n"
1105
+ . "# Natural Docs will be able to correct the locations automatically.\n"
1106
+ . "# \n";
1107
+ };
1108
+
1109
+ print MENUFILEHANDLE
1110
+ "# --------------------------------------------------------------------------\n"
1111
+
1112
+ . "\n\n";
1113
+
1114
+
1115
+ $self->WriteMenuEntries($menu->GroupContent(), \*MENUFILEHANDLE, undef, (scalar @$inputDirs == 1));
1116
+
1117
+
1118
+ if (scalar @$inputDirs > 1)
1119
+ {
1120
+ print MENUFILEHANDLE
1121
+ "\n\n##### Do not change or remove these lines. #####\n";
1122
+
1123
+ foreach my $inputDir (@$inputDirs)
1124
+ {
1125
+ print MENUFILEHANDLE
1126
+ 'Data: 1(' . NaturalDocs::ConfigFile->Obscure( NaturalDocs::Settings->InputDirectoryNameOf($inputDir)
1127
+ . '///' . $inputDir ) . ")\n";
1128
+ };
1129
+ }
1130
+ elsif (lc(NaturalDocs::Settings->InputDirectoryNameOf($inputDirs->[0])) != 1)
1131
+ {
1132
+ print MENUFILEHANDLE
1133
+ "\n\n##### Do not change or remove this line. #####\n"
1134
+ . 'Data: 2(' . NaturalDocs::ConfigFile->Obscure( NaturalDocs::Settings->InputDirectoryNameOf($inputDirs->[0]) ) . ")\n";
1135
+ }
1136
+
1137
+ close(MENUFILEHANDLE);
1138
+ };
1139
+
1140
+
1141
+ #
1142
+ # Function: WriteMenuEntries
1143
+ #
1144
+ # A recursive function to write the contents of an arrayref of <NaturalDocs::Menu::Entry> objects to disk.
1145
+ #
1146
+ # Parameters:
1147
+ #
1148
+ # entries - The arrayref of menu entries to write.
1149
+ # fileHandle - The handle to the output file.
1150
+ # indentChars - The indentation _characters_ to add before each line. It is not the number of characters, it is the characters
1151
+ # themselves. Use undef for none.
1152
+ # relativeFiles - Whether to use relative file names.
1153
+ #
1154
+ sub WriteMenuEntries #(entries, fileHandle, indentChars, relativeFiles)
1155
+ {
1156
+ my ($self, $entries, $fileHandle, $indentChars, $relativeFiles) = @_;
1157
+ my $lastEntryType;
1158
+
1159
+ foreach my $entry (@$entries)
1160
+ {
1161
+ if ($entry->Type() == ::MENU_FILE())
1162
+ {
1163
+ my $fileName;
1164
+
1165
+ if ($relativeFiles)
1166
+ { $fileName = (NaturalDocs::Settings->SplitFromInputDirectory($entry->Target()))[1]; }
1167
+ else
1168
+ { $fileName = $entry->Target(); };
1169
+
1170
+ print $fileHandle $indentChars . 'File: ' . $self->ConvertAmpChars( $entry->Title(), CONVERT_PARENTHESIS() )
1171
+ . ' (' . ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE() ? 'no auto-title, ' : '')
1172
+ . $self->ConvertAmpChars($fileName) . ")\n";
1173
+ }
1174
+ elsif ($entry->Type() == ::MENU_GROUP())
1175
+ {
1176
+ if (defined $lastEntryType && $lastEntryType != ::MENU_GROUP())
1177
+ { print $fileHandle "\n"; };
1178
+
1179
+ print $fileHandle $indentChars . 'Group: ' . $self->ConvertAmpChars( $entry->Title() ) . " {\n\n";
1180
+ $self->WriteMenuEntries($entry->GroupContent(), $fileHandle, ' ' . $indentChars, $relativeFiles);
1181
+ print $fileHandle ' ' . $indentChars . '} # Group: ' . $self->ConvertAmpChars( $entry->Title() ) . "\n\n";
1182
+ }
1183
+ elsif ($entry->Type() == ::MENU_TEXT())
1184
+ {
1185
+ print $fileHandle $indentChars . 'Text: ' . $self->ConvertAmpChars( $entry->Title() ) . "\n";
1186
+ }
1187
+ elsif ($entry->Type() == ::MENU_LINK())
1188
+ {
1189
+ print $fileHandle $indentChars . 'Link: ' . $self->ConvertAmpChars( $entry->Title() ) . ' '
1190
+ . '(' . $self->ConvertAmpChars( $entry->Target(), CONVERT_PARENTHESIS() ) . ')' . "\n";
1191
+ }
1192
+ elsif ($entry->Type() == ::MENU_INDEX())
1193
+ {
1194
+ my $type;
1195
+ if ($entry->Target() ne ::TOPIC_GENERAL())
1196
+ {
1197
+ $type = NaturalDocs::Topics->NameOfType($entry->Target()) . ' ';
1198
+ };
1199
+
1200
+ print $fileHandle $indentChars . $self->ConvertAmpChars($type, CONVERT_COLONS()) . 'Index: '
1201
+ . $self->ConvertAmpChars( $entry->Title() ) . "\n";
1202
+ };
1203
+
1204
+ $lastEntryType = $entry->Type();
1205
+ };
1206
+ };
1207
+
1208
+
1209
+ #
1210
+ # Function: LoadPreviousMenuStateFile
1211
+ #
1212
+ # Loads and parses the previous menu state file.
1213
+ #
1214
+ # Returns:
1215
+ #
1216
+ # The array ( previousMenu, previousIndexes, previousFiles ) or an empty array if there was a problem with the file.
1217
+ #
1218
+ # previousMenu - A <MENU_GROUP> <NaturalDocs::Menu::Entry> object, similar to <menu>, which contains the entire
1219
+ # previous menu.
1220
+ # previousIndexes - An existence hashref of the index <TopicTypes> present in the previous menu.
1221
+ # previousFiles - A hashref of the files present in the previous menu. The keys are the <FileNames>, and the entries are
1222
+ # references to its object in previousMenu.
1223
+ #
1224
+ sub LoadPreviousMenuStateFile
1225
+ {
1226
+ my ($self) = @_;
1227
+
1228
+ my $fileIsOkay;
1229
+ my $version;
1230
+ my $previousStateFileName = NaturalDocs::Project->DataFile('PreviousMenuState.nd');
1231
+
1232
+ if (open(PREVIOUSSTATEFILEHANDLE, '<' . $previousStateFileName))
1233
+ {
1234
+ # See if it's binary.
1235
+ binmode(PREVIOUSSTATEFILEHANDLE);
1236
+
1237
+ my $firstChar;
1238
+ read(PREVIOUSSTATEFILEHANDLE, $firstChar, 1);
1239
+
1240
+ if ($firstChar == ::BINARY_FORMAT())
1241
+ {
1242
+ $version = NaturalDocs::Version->FromBinaryFile(\*PREVIOUSSTATEFILEHANDLE);
1243
+
1244
+ # Only the topic type format has changed since switching to binary, and we support both methods.
1245
+
1246
+ if (NaturalDocs::Version->CheckFileFormat($version))
1247
+ { $fileIsOkay = 1; }
1248
+ else
1249
+ { close(PREVIOUSSTATEFILEHANDLE); };
1250
+ }
1251
+
1252
+ else # it's not in binary
1253
+ { close(PREVIOUSSTATEFILEHANDLE); };
1254
+ };
1255
+
1256
+ if ($fileIsOkay)
1257
+ {
1258
+ if (NaturalDocs::Project->UserConfigFileStatus('Menu.txt') == ::FILE_CHANGED())
1259
+ { $hasChanged = 1; };
1260
+
1261
+
1262
+ my $menu = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), undef, undef, undef);
1263
+ my $indexes = { };
1264
+ my $files = { };
1265
+
1266
+ my @groupStack;
1267
+ my $currentGroup = $menu;
1268
+ my $raw;
1269
+
1270
+ # [UInt8: type or 0 for end group]
1271
+
1272
+ while (read(PREVIOUSSTATEFILEHANDLE, $raw, 1))
1273
+ {
1274
+ my ($type, $flags, $title, $titleLength, $target, $targetLength);
1275
+ $type = unpack('C', $raw);
1276
+
1277
+ if ($type == 0)
1278
+ { $currentGroup = pop @groupStack; }
1279
+
1280
+ elsif ($type == ::MENU_FILE())
1281
+ {
1282
+ # [UInt8: noAutoTitle] [AString16: title] [AString16: target]
1283
+
1284
+ read(PREVIOUSSTATEFILEHANDLE, $raw, 3);
1285
+ (my $noAutoTitle, $titleLength) = unpack('Cn', $raw);
1286
+
1287
+ if ($noAutoTitle)
1288
+ { $flags = ::MENU_FILE_NOAUTOTITLE(); };
1289
+
1290
+ read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
1291
+ read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
1292
+
1293
+ $targetLength = unpack('n', $raw);
1294
+
1295
+ read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength);
1296
+ }
1297
+
1298
+ elsif ($type == ::MENU_GROUP())
1299
+ {
1300
+ # [AString16: title]
1301
+
1302
+ read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
1303
+ $titleLength = unpack('n', $raw);
1304
+
1305
+ read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
1306
+ }
1307
+
1308
+ elsif ($type == ::MENU_INDEX())
1309
+ {
1310
+ # [AString16: title]
1311
+
1312
+ read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
1313
+ $titleLength = unpack('n', $raw);
1314
+
1315
+ read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
1316
+
1317
+ if ($version >= NaturalDocs::Version->FromString('1.3'))
1318
+ {
1319
+ # [AString16: topic type]
1320
+ read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
1321
+ $targetLength = unpack('n', $raw);
1322
+
1323
+ read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength);
1324
+ }
1325
+ else
1326
+ {
1327
+ # [UInt8: topic type (0 for general)]
1328
+ read(PREVIOUSSTATEFILEHANDLE, $raw, 1);
1329
+ $target = unpack('C', $raw);
1330
+
1331
+ $target = NaturalDocs::Topics->TypeFromLegacy($target);
1332
+ };
1333
+ }
1334
+
1335
+ elsif ($type == ::MENU_LINK())
1336
+ {
1337
+ # [AString16: title] [AString16: url]
1338
+
1339
+ read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
1340
+ $titleLength = unpack('n', $raw);
1341
+
1342
+ read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
1343
+ read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
1344
+ $targetLength = unpack('n', $raw);
1345
+
1346
+ read(PREVIOUSSTATEFILEHANDLE, $target, $targetLength);
1347
+ }
1348
+
1349
+ elsif ($type == ::MENU_TEXT())
1350
+ {
1351
+ # [AString16: text]
1352
+
1353
+ read(PREVIOUSSTATEFILEHANDLE, $raw, 2);
1354
+ $titleLength = unpack('n', $raw);
1355
+
1356
+ read(PREVIOUSSTATEFILEHANDLE, $title, $titleLength);
1357
+ };
1358
+
1359
+
1360
+ # The topic type of the index may have been removed.
1361
+
1362
+ if ( !($type == ::MENU_INDEX() && !NaturalDocs::Topics->IsValidType($target)) )
1363
+ {
1364
+ my $entry = NaturalDocs::Menu::Entry->New($type, $title, $target, ($flags || 0));
1365
+ $currentGroup->PushToGroup($entry);
1366
+
1367
+ if ($type == ::MENU_FILE())
1368
+ {
1369
+ $files->{$target} = $entry;
1370
+ }
1371
+ elsif ($type == ::MENU_GROUP())
1372
+ {
1373
+ push @groupStack, $currentGroup;
1374
+ $currentGroup = $entry;
1375
+ }
1376
+ elsif ($type == ::MENU_INDEX())
1377
+ {
1378
+ $indexes->{$target} = 1;
1379
+ };
1380
+ };
1381
+
1382
+ };
1383
+
1384
+ close(PREVIOUSSTATEFILEHANDLE);
1385
+
1386
+ return ($menu, $indexes, $files);
1387
+ }
1388
+ else
1389
+ {
1390
+ $hasChanged = 1;
1391
+ return ( );
1392
+ };
1393
+ };
1394
+
1395
+
1396
+ #
1397
+ # Function: SavePreviousMenuStateFile
1398
+ #
1399
+ # Saves changes to <PreviousMenuState.nd>.
1400
+ #
1401
+ sub SavePreviousMenuStateFile
1402
+ {
1403
+ my ($self) = @_;
1404
+
1405
+ open (PREVIOUSSTATEFILEHANDLE, '>' . NaturalDocs::Project->DataFile('PreviousMenuState.nd'))
1406
+ or die "Couldn't save " . NaturalDocs::Project->DataFile('PreviousMenuState.nd') . ".\n";
1407
+
1408
+ binmode(PREVIOUSSTATEFILEHANDLE);
1409
+
1410
+ print PREVIOUSSTATEFILEHANDLE '' . ::BINARY_FORMAT();
1411
+
1412
+ NaturalDocs::Version->ToBinaryFile(\*PREVIOUSSTATEFILEHANDLE, NaturalDocs::Settings->AppVersion());
1413
+
1414
+ $self->WritePreviousMenuStateEntries($menu->GroupContent(), \*PREVIOUSSTATEFILEHANDLE);
1415
+
1416
+ close(PREVIOUSSTATEFILEHANDLE);
1417
+ };
1418
+
1419
+
1420
+ #
1421
+ # Function: WritePreviousMenuStateEntries
1422
+ #
1423
+ # A recursive function to write the contents of an arrayref of <NaturalDocs::Menu::Entry> objects to disk.
1424
+ #
1425
+ # Parameters:
1426
+ #
1427
+ # entries - The arrayref of menu entries to write.
1428
+ # fileHandle - The handle to the output file.
1429
+ #
1430
+ sub WritePreviousMenuStateEntries #(entries, fileHandle)
1431
+ {
1432
+ my ($self, $entries, $fileHandle) = @_;
1433
+
1434
+ foreach my $entry (@$entries)
1435
+ {
1436
+ if ($entry->Type() == ::MENU_FILE())
1437
+ {
1438
+ # We need to do length manually instead of using n/A in the template because it's not supported in earlier versions
1439
+ # of Perl.
1440
+
1441
+ # [UInt8: MENU_FILE] [UInt8: noAutoTitle] [AString16: title] [AString16: target]
1442
+ print $fileHandle pack('CCnA*nA*', ::MENU_FILE(), ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE() ? 1 : 0),
1443
+ length($entry->Title()), $entry->Title(),
1444
+ length($entry->Target()), $entry->Target());
1445
+ }
1446
+
1447
+ elsif ($entry->Type() == ::MENU_GROUP())
1448
+ {
1449
+ # [UInt8: MENU_GROUP] [AString16: title]
1450
+ print $fileHandle pack('CnA*', ::MENU_GROUP(), length($entry->Title()), $entry->Title());
1451
+ $self->WritePreviousMenuStateEntries($entry->GroupContent(), $fileHandle);
1452
+ print $fileHandle pack('C', 0);
1453
+ }
1454
+
1455
+ elsif ($entry->Type() == ::MENU_INDEX())
1456
+ {
1457
+ # [UInt8: MENU_INDEX] [AString16: title] [AString16: topic type]
1458
+ print $fileHandle pack('CnA*nA*', ::MENU_INDEX(), length($entry->Title()), $entry->Title(),
1459
+ length($entry->Target()), $entry->Target());
1460
+ }
1461
+
1462
+ elsif ($entry->Type() == ::MENU_LINK())
1463
+ {
1464
+ # [UInt8: MENU_LINK] [AString16: title] [AString16: url]
1465
+ print $fileHandle pack('CnA*nA*', ::MENU_LINK(), length($entry->Title()), $entry->Title(),
1466
+ length($entry->Target()), $entry->Target());
1467
+ }
1468
+
1469
+ elsif ($entry->Type() == ::MENU_TEXT())
1470
+ {
1471
+ # [UInt8: MENU_TEXT] [AString16: hext]
1472
+ print $fileHandle pack('CnA*', ::MENU_TEXT(), length($entry->Title()), $entry->Title());
1473
+ };
1474
+ };
1475
+
1476
+ };
1477
+
1478
+
1479
+ #
1480
+ # Function: CheckForTrashedMenu
1481
+ #
1482
+ # Checks the menu to see if a significant number of file entries didn't resolve to actual files, and if so, saves a backup of the
1483
+ # menu and issues a warning.
1484
+ #
1485
+ # Parameters:
1486
+ #
1487
+ # numberOriginallyInMenu - A count of how many file entries were in the menu orignally.
1488
+ # numberRemoved - A count of how many file entries were removed from the menu.
1489
+ #
1490
+ sub CheckForTrashedMenu #(numberOriginallyInMenu, numberRemoved)
1491
+ {
1492
+ my ($self, $numberOriginallyInMenu, $numberRemoved) = @_;
1493
+
1494
+ no integer;
1495
+
1496
+ if ( ($numberOriginallyInMenu >= 6 && $numberRemoved == $numberOriginallyInMenu) ||
1497
+ ($numberOriginallyInMenu >= 12 && ($numberRemoved / $numberOriginallyInMenu) >= 0.4) ||
1498
+ ($numberRemoved >= 15) )
1499
+ {
1500
+ my $backupFile = NaturalDocs::Project->UserConfigFile('Menu_Backup.txt');
1501
+ my $backupFileNumber = 1;
1502
+
1503
+ while (-e $backupFile)
1504
+ {
1505
+ $backupFileNumber++;
1506
+ $backupFile = NaturalDocs::Project->UserConfigFile('Menu_Backup_' . $backupFileNumber . '.txt');
1507
+ };
1508
+
1509
+ NaturalDocs::File->Copy( NaturalDocs::Project->UserConfigFile('Menu.txt'), $backupFile );
1510
+
1511
+ print STDERR
1512
+ "\n"
1513
+ # GNU format. See http://www.gnu.org/prep/standards_15.html
1514
+ . "NaturalDocs: warning: possible trashed menu\n"
1515
+ . "\n"
1516
+ . " Natural Docs has detected that a significant number file entries in the\n"
1517
+ . " menu did not resolve to actual files. A backup of your original menu file\n"
1518
+ . " has been saved as\n"
1519
+ . "\n"
1520
+ . " " . $backupFile . "\n"
1521
+ . "\n"
1522
+ . " - If you recently deleted a lot of files from your project, you can safely\n"
1523
+ . " ignore this message. They have been deleted from the menu as well.\n"
1524
+ . " - If you recently rearranged your source tree, you may want to restore your\n"
1525
+ . " menu from the backup and do a search and replace to preserve your layout.\n"
1526
+ . " Otherwise the position of any moved files will be reset.\n"
1527
+ . " - If neither of these is the case, you may have gotten the -i parameter\n"
1528
+ . " wrong in the command line. You should definitely restore the backup and\n"
1529
+ . " try again, because otherwise every file in your menu will be reset.\n"
1530
+ . "\n";
1531
+ };
1532
+
1533
+ use integer;
1534
+ };
1535
+
1536
+
1537
+ #
1538
+ # Function: GenerateTimestampText
1539
+ #
1540
+ # Generates <timestampText> from <timestampCode> with the current date.
1541
+ #
1542
+ sub GenerateTimestampText
1543
+ {
1544
+ my $self = shift;
1545
+
1546
+ my @longMonths = ( 'January', 'February', 'March', 'April', 'May', 'June',
1547
+ 'July', 'August', 'September', 'October', 'November', 'December' );
1548
+ my @shortMonths = ( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec' );
1549
+
1550
+ my (undef, undef, undef, $day, $month, $year) = localtime();
1551
+ $year += 1900;
1552
+
1553
+ my $longDay;
1554
+ if ($day % 10 == 1 && $day != 11)
1555
+ { $longDay = $day . 'st'; }
1556
+ elsif ($day % 10 == 2 && $day != 12)
1557
+ { $longDay = $day . 'nd'; }
1558
+ elsif ($day % 10 == 3 && $day != 13)
1559
+ { $longDay = $day . 'rd'; }
1560
+ else
1561
+ { $longDay = $day . 'th'; };
1562
+
1563
+
1564
+ $timestampText = $timestampCode;
1565
+
1566
+ $timestampText =~ s/(?<![a-z])month(?![a-z])/$longMonths[$month]/i;
1567
+ $timestampText =~ s/(?<![a-z])mon(?![a-z])/$shortMonths[$month]/i;
1568
+ $timestampText =~ s/(?<![a-z])mm(?![a-z])/sprintf('%02d', $month + 1)/ie;
1569
+ $timestampText =~ s/(?<![a-z])m(?![a-z])/$month + 1/ie;
1570
+
1571
+ $timestampText =~ s/(?<![a-z])day(?![a-z])/$longDay/i;
1572
+ $timestampText =~ s/(?<![a-z])dd(?![a-z])/sprintf('%02d', $day)/ie;
1573
+ $timestampText =~ s/(?<![a-z])d(?![a-z])/$day/i;
1574
+
1575
+ $timestampText =~ s/(?<![a-z])(?:year|yyyy)(?![a-z])/$year/i;
1576
+ $timestampText =~ s/(?<![a-z])(?:year|yyyy)(?![a-z])/$year/i; #XXX
1577
+ $timestampText =~ s/(?<![a-z])yy(?![a-z])/sprintf('%02d', $year % 100)/ie;
1578
+ };
1579
+
1580
+
1581
+ use constant CONVERT_PARENTHESIS => 0x01;
1582
+ use constant CONVERT_COMMAS => 0x02;
1583
+ use constant CONVERT_COLONS => 0x04;
1584
+
1585
+ #
1586
+ # Function: ConvertAmpChars
1587
+ # Replaces certain characters in the string with their entities and returns it.
1588
+ #
1589
+ # Parameters:
1590
+ #
1591
+ # text - The text to convert.
1592
+ # flags - The flags of any additional characters to convert.
1593
+ #
1594
+ # Flags:
1595
+ #
1596
+ # - CONVERT_PARENTHESIS
1597
+ # - CONVERT_COMMAS
1598
+ # - CONVERT_COLONS
1599
+ #
1600
+ # Returns:
1601
+ #
1602
+ # The string with the amp chars converted.
1603
+ #
1604
+ sub ConvertAmpChars #(string text, int flags) => string
1605
+ {
1606
+ my ($self, $text, $flags) = @_;
1607
+
1608
+ $text =~ s/&/&amp;/g;
1609
+ $text =~ s/\{/&lbrace;/g;
1610
+ $text =~ s/\}/&rbrace;/g;
1611
+
1612
+ if ($flags & CONVERT_PARENTHESIS())
1613
+ {
1614
+ $text =~ s/\(/&lparen;/g;
1615
+ $text =~ s/\)/&rparen;/g;
1616
+ };
1617
+ if ($flags & CONVERT_COMMAS())
1618
+ {
1619
+ $text =~ s/\,/&comma;/g;
1620
+ };
1621
+ if ($flags & CONVERT_COLONS())
1622
+ {
1623
+ $text =~ s/\:/&colon;/g;
1624
+ };
1625
+
1626
+ return $text;
1627
+ };
1628
+
1629
+
1630
+ #
1631
+ # Function: RestoreAmpChars
1632
+ # Replaces entity characters in the string with their original characters and returns it. This will restore all amp chars regardless
1633
+ # of the flags passed to <ConvertAmpChars()>.
1634
+ #
1635
+ sub RestoreAmpChars #(string text) => string
1636
+ {
1637
+ my ($self, $text) = @_;
1638
+
1639
+ $text =~ s/&lparen;/(/gi;
1640
+ $text =~ s/&rparen;/)/gi;
1641
+ $text =~ s/&lbrace;/{/gi;
1642
+ $text =~ s/&rbrace;/}/gi;
1643
+ $text =~ s/&comma;/,/gi;
1644
+ $text =~ s/&amp;/&/gi;
1645
+ $text =~ s/&colon;/:/gi;
1646
+
1647
+ return $text;
1648
+ };
1649
+
1650
+
1651
+
1652
+ ###############################################################################
1653
+ # Group: Auto-Adjustment Functions
1654
+
1655
+
1656
+ #
1657
+ # Function: ResolveInputDirectories
1658
+ #
1659
+ # Detects if the input directories in the menu file match those in the command line, and if not, tries to resolve them. This allows
1660
+ # menu files to work across machines, since the absolute paths won't be the same but the relative ones should be.
1661
+ #
1662
+ # Parameters:
1663
+ #
1664
+ # inputDirectoryNames - A hashref of the input directories appearing in the menu file, or undef if none. The keys are the
1665
+ # directories, and the values are their names. May be undef.
1666
+ #
1667
+ sub ResolveInputDirectories #(inputDirectoryNames)
1668
+ {
1669
+ my ($self, $menuDirectoryNames) = @_;
1670
+
1671
+
1672
+ # Determine which directories don't match the command line, if any.
1673
+
1674
+ my $inputDirectories = NaturalDocs::Settings->InputDirectories();
1675
+ my @unresolvedMenuDirectories;
1676
+
1677
+ foreach my $menuDirectory (keys %$menuDirectoryNames)
1678
+ {
1679
+ my $found;
1680
+
1681
+ foreach my $inputDirectory (@$inputDirectories)
1682
+ {
1683
+ if ($menuDirectory eq $inputDirectory)
1684
+ {
1685
+ $found = 1;
1686
+ last;
1687
+ };
1688
+ };
1689
+
1690
+ if (!$found)
1691
+ { push @unresolvedMenuDirectories, $menuDirectory; };
1692
+ };
1693
+
1694
+ # Quit if everything matches up, which should be the most common case.
1695
+ if (!scalar @unresolvedMenuDirectories)
1696
+ { return; };
1697
+
1698
+ # Poop. See which input directories are still available.
1699
+
1700
+ my @unresolvedInputDirectories;
1701
+
1702
+ foreach my $inputDirectory (@$inputDirectories)
1703
+ {
1704
+ if (!exists $menuDirectoryNames->{$inputDirectory})
1705
+ { push @unresolvedInputDirectories, $inputDirectory; };
1706
+ };
1707
+
1708
+ # Quit if there are none. This means an input directory is in the menu that isn't in the command line. Natural Docs should
1709
+ # proceed normally and let the files be deleted.
1710
+ if (!scalar @unresolvedInputDirectories)
1711
+ {
1712
+ $hasChanged = 1;
1713
+ return;
1714
+ };
1715
+
1716
+ # The index into menuDirectoryScores is the same as in unresolvedMenuDirectories. The index into each arrayref within it is
1717
+ # the same as in unresolvedInputDirectories.
1718
+ my @menuDirectoryScores;
1719
+ for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++)
1720
+ { push @menuDirectoryScores, [ ]; };
1721
+
1722
+
1723
+ # Now plow through the menu, looking for files that have an unresolved base.
1724
+
1725
+ my @menuGroups = ( $menu );
1726
+
1727
+ while (scalar @menuGroups)
1728
+ {
1729
+ my $currentGroup = pop @menuGroups;
1730
+ my $currentGroupContent = $currentGroup->GroupContent();
1731
+
1732
+ foreach my $entry (@$currentGroupContent)
1733
+ {
1734
+ if ($entry->Type() == ::MENU_GROUP())
1735
+ {
1736
+ push @menuGroups, $entry;
1737
+ }
1738
+ elsif ($entry->Type() == ::MENU_FILE())
1739
+ {
1740
+ # Check if it uses an unresolved base.
1741
+ for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++)
1742
+ {
1743
+ if (NaturalDocs::File->IsSubPathOf($unresolvedMenuDirectories[$i], $entry->Target()))
1744
+ {
1745
+ my $relativePath = NaturalDocs::File->MakeRelativePath($unresolvedMenuDirectories[$i], $entry->Target());
1746
+ $self->ResolveFile($relativePath, \@unresolvedInputDirectories, $menuDirectoryScores[$i]);
1747
+ last;
1748
+ };
1749
+ };
1750
+ };
1751
+ };
1752
+ };
1753
+
1754
+
1755
+ # Now, create an array of score objects. Each score object is the three value arrayref [ from, to, score ]. From and To are the
1756
+ # conversion options and are the indexes into unresolvedInput/MenuDirectories. We'll sort this array by score to get the best
1757
+ # possible conversions. Yes, really.
1758
+ my @scores;
1759
+
1760
+ for (my $menuIndex = 0; $menuIndex < scalar @unresolvedMenuDirectories; $menuIndex++)
1761
+ {
1762
+ for (my $inputIndex = 0; $inputIndex < scalar @unresolvedInputDirectories; $inputIndex++)
1763
+ {
1764
+ if ($menuDirectoryScores[$menuIndex]->[$inputIndex])
1765
+ {
1766
+ push @scores, [ $menuIndex, $inputIndex, $menuDirectoryScores[$menuIndex]->[$inputIndex] ];
1767
+ };
1768
+ };
1769
+ };
1770
+
1771
+ @scores = sort { $b->[2] <=> $a->[2] } @scores;
1772
+
1773
+
1774
+ # Now we determine what goes where.
1775
+ my @menuDirectoryConversions;
1776
+
1777
+ foreach my $scoreObject (@scores)
1778
+ {
1779
+ if (!defined $menuDirectoryConversions[ $scoreObject->[0] ])
1780
+ {
1781
+ $menuDirectoryConversions[ $scoreObject->[0] ] = $unresolvedInputDirectories[ $scoreObject->[1] ];
1782
+ };
1783
+ };
1784
+
1785
+
1786
+ # Now, FINALLY, we do the conversion. Note that not every menu directory may have a conversion defined.
1787
+
1788
+ @menuGroups = ( $menu );
1789
+
1790
+ while (scalar @menuGroups)
1791
+ {
1792
+ my $currentGroup = pop @menuGroups;
1793
+ my $currentGroupContent = $currentGroup->GroupContent();
1794
+
1795
+ foreach my $entry (@$currentGroupContent)
1796
+ {
1797
+ if ($entry->Type() == ::MENU_GROUP())
1798
+ {
1799
+ push @menuGroups, $entry;
1800
+ }
1801
+ elsif ($entry->Type() == ::MENU_FILE())
1802
+ {
1803
+ # Check if it uses an unresolved base.
1804
+ for (my $i = 0; $i < scalar @unresolvedMenuDirectories; $i++)
1805
+ {
1806
+ if (NaturalDocs::File->IsSubPathOf($unresolvedMenuDirectories[$i], $entry->Target()) &&
1807
+ defined $menuDirectoryConversions[$i])
1808
+ {
1809
+ my $relativePath = NaturalDocs::File->MakeRelativePath($unresolvedMenuDirectories[$i], $entry->Target());
1810
+ $entry->SetTarget( NaturalDocs::File->JoinPaths($menuDirectoryConversions[$i], $relativePath) );
1811
+ last;
1812
+ };
1813
+ };
1814
+ };
1815
+ };
1816
+ };
1817
+
1818
+
1819
+ # Whew.
1820
+
1821
+ $hasChanged = 1;
1822
+ };
1823
+
1824
+
1825
+ #
1826
+ # Function: ResolveRelativeInputDirectories
1827
+ #
1828
+ # Resolves relative input directories to the input directories available.
1829
+ #
1830
+ sub ResolveRelativeInputDirectories
1831
+ {
1832
+ my ($self) = @_;
1833
+
1834
+ my $inputDirectories = NaturalDocs::Settings->InputDirectories();
1835
+ my $resolvedInputDirectory;
1836
+
1837
+ if (scalar @$inputDirectories == 1)
1838
+ { $resolvedInputDirectory = $inputDirectories->[0]; }
1839
+ else
1840
+ {
1841
+ my @score;
1842
+
1843
+ # Plow through the menu, looking for files and scoring them.
1844
+
1845
+ my @menuGroups = ( $menu );
1846
+
1847
+ while (scalar @menuGroups)
1848
+ {
1849
+ my $currentGroup = pop @menuGroups;
1850
+ my $currentGroupContent = $currentGroup->GroupContent();
1851
+
1852
+ foreach my $entry (@$currentGroupContent)
1853
+ {
1854
+ if ($entry->Type() == ::MENU_GROUP())
1855
+ {
1856
+ push @menuGroups, $entry;
1857
+ }
1858
+ elsif ($entry->Type() == ::MENU_FILE())
1859
+ {
1860
+ $self->ResolveFile($entry->Target(), $inputDirectories, \@score);
1861
+ };
1862
+ };
1863
+ };
1864
+
1865
+ # Determine the best match.
1866
+
1867
+ my $bestScore = 0;
1868
+ my $bestIndex = 0;
1869
+
1870
+ for (my $i = 0; $i < scalar @$inputDirectories; $i++)
1871
+ {
1872
+ if ($score[$i] > $bestScore)
1873
+ {
1874
+ $bestScore = $score[$i];
1875
+ $bestIndex = $i;
1876
+ };
1877
+ };
1878
+
1879
+ $resolvedInputDirectory = $inputDirectories->[$bestIndex];
1880
+ };
1881
+
1882
+
1883
+ # Okay, now that we have our resolved directory, update everything.
1884
+
1885
+ my @menuGroups = ( $menu );
1886
+
1887
+ while (scalar @menuGroups)
1888
+ {
1889
+ my $currentGroup = pop @menuGroups;
1890
+ my $currentGroupContent = $currentGroup->GroupContent();
1891
+
1892
+ foreach my $entry (@$currentGroupContent)
1893
+ {
1894
+ if ($entry->Type() == ::MENU_GROUP())
1895
+ { push @menuGroups, $entry; }
1896
+ elsif ($entry->Type() == ::MENU_FILE())
1897
+ {
1898
+ $entry->SetTarget( NaturalDocs::File->JoinPaths($resolvedInputDirectory, $entry->Target()) );
1899
+ };
1900
+ };
1901
+ };
1902
+
1903
+ if (scalar @$inputDirectories > 1)
1904
+ { $hasChanged = 1; };
1905
+
1906
+ return $resolvedInputDirectory;
1907
+ };
1908
+
1909
+
1910
+ #
1911
+ # Function: ResolveFile
1912
+ #
1913
+ # Tests a relative path against a list of directories. Adds one to the score of each base where there is a match.
1914
+ #
1915
+ # Parameters:
1916
+ #
1917
+ # relativePath - The relative file name to test.
1918
+ # possibleBases - An arrayref of bases to test it against.
1919
+ # possibleBaseScores - An arrayref of scores to adjust. The score indexes should correspond to the base indexes.
1920
+ #
1921
+ sub ResolveFile #(relativePath, possibleBases, possibleBaseScores)
1922
+ {
1923
+ my ($self, $relativePath, $possibleBases, $possibleBaseScores) = @_;
1924
+
1925
+ for (my $i = 0; $i < scalar @$possibleBases; $i++)
1926
+ {
1927
+ if (-e NaturalDocs::File->JoinPaths($possibleBases->[$i], $relativePath))
1928
+ { $possibleBaseScores->[$i]++; };
1929
+ };
1930
+ };
1931
+
1932
+
1933
+ #
1934
+ # Function: LockUserTitleChanges
1935
+ #
1936
+ # Detects if the user manually changed any file titles, and if so, automatically locks them with <MENU_FILE_NOAUTOTITLE>.
1937
+ #
1938
+ # Parameters:
1939
+ #
1940
+ # previousMenuFiles - A hashref of the files from the previous menu state. The keys are the <FileNames>, and the values are
1941
+ # references to their <NaturalDocs::Menu::Entry> objects.
1942
+ #
1943
+ sub LockUserTitleChanges #(previousMenuFiles)
1944
+ {
1945
+ my ($self, $previousMenuFiles) = @_;
1946
+
1947
+ my @groupStack = ( $menu );
1948
+ my $groupEntry;
1949
+
1950
+ while (scalar @groupStack)
1951
+ {
1952
+ $groupEntry = pop @groupStack;
1953
+
1954
+ foreach my $entry (@{$groupEntry->GroupContent()})
1955
+ {
1956
+
1957
+ # If it's an unlocked file entry
1958
+ if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0)
1959
+ {
1960
+ my $previousEntry = $previousMenuFiles->{$entry->Target()};
1961
+
1962
+ # If the previous entry was also unlocked and the titles are different, the user changed the title. Automatically lock it.
1963
+ if (defined $previousEntry && ($previousEntry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 &&
1964
+ $entry->Title() ne $previousEntry->Title())
1965
+ {
1966
+ $entry->SetFlags($entry->Flags() | ::MENU_FILE_NOAUTOTITLE());
1967
+ $hasChanged = 1;
1968
+ };
1969
+ }
1970
+
1971
+ elsif ($entry->Type() == ::MENU_GROUP())
1972
+ {
1973
+ push @groupStack, $entry;
1974
+ };
1975
+
1976
+ };
1977
+ };
1978
+ };
1979
+
1980
+
1981
+ #
1982
+ # Function: FlagAutoTitleChanges
1983
+ #
1984
+ # Finds which files have auto-titles that changed and flags their groups for updating with <MENU_GROUP_UPDATETITLES> and
1985
+ # <MENU_GROUP_UPDATEORDER>.
1986
+ #
1987
+ sub FlagAutoTitleChanges
1988
+ {
1989
+ my ($self) = @_;
1990
+
1991
+ my @groupStack = ( $menu );
1992
+ my $groupEntry;
1993
+
1994
+ while (scalar @groupStack)
1995
+ {
1996
+ $groupEntry = pop @groupStack;
1997
+
1998
+ foreach my $entry (@{$groupEntry->GroupContent()})
1999
+ {
2000
+ if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 &&
2001
+ exists $defaultTitlesChanged{$entry->Target()})
2002
+ {
2003
+ $groupEntry->SetFlags($groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() | ::MENU_GROUP_UPDATEORDER());
2004
+ $hasChanged = 1;
2005
+ }
2006
+ elsif ($entry->Type() == ::MENU_GROUP())
2007
+ {
2008
+ push @groupStack, $entry;
2009
+ };
2010
+ };
2011
+ };
2012
+ };
2013
+
2014
+
2015
+ #
2016
+ # Function: AutoPlaceNewFiles
2017
+ #
2018
+ # Adds files to the menu that aren't already on it, attempting to guess where they belong.
2019
+ #
2020
+ # New files are placed after a dummy <MENU_ENDOFORIGINAL> entry so that they don't affect the detected order. Also, the
2021
+ # groups they're placed in get <MENU_GROUP_UPDATETITLES>, <MENU_GROUP_UPDATESTRUCTURE>, and
2022
+ # <MENU_GROUP_UPDATEORDER> flags.
2023
+ #
2024
+ # Parameters:
2025
+ #
2026
+ # filesInMenu - An existence hash of all the <FileNames> present in the menu.
2027
+ #
2028
+ sub AutoPlaceNewFiles #(fileInMenu)
2029
+ {
2030
+ my ($self, $filesInMenu) = @_;
2031
+
2032
+ my $files = NaturalDocs::Project->FilesWithContent();
2033
+
2034
+ my $directories;
2035
+
2036
+ foreach my $file (keys %$files)
2037
+ {
2038
+ if (!exists $filesInMenu->{$file})
2039
+ {
2040
+ # This is done on demand because new files shouldn't be added very often, so this will save time.
2041
+ if (!defined $directories)
2042
+ { $directories = $self->MatchDirectoriesAndGroups(); };
2043
+
2044
+ my $targetGroup;
2045
+ my $fileDirectoryString = (NaturalDocs::File->SplitPath($file))[1];
2046
+
2047
+ $targetGroup = $directories->{$fileDirectoryString};
2048
+
2049
+ if (!defined $targetGroup)
2050
+ {
2051
+ # Okay, if there's no exact match, work our way down.
2052
+
2053
+ my @fileDirectories = NaturalDocs::File->SplitDirectories($fileDirectoryString);
2054
+
2055
+ do
2056
+ {
2057
+ pop @fileDirectories;
2058
+ $targetGroup = $directories->{ NaturalDocs::File->JoinDirectories(@fileDirectories) };
2059
+ }
2060
+ while (!defined $targetGroup && scalar @fileDirectories);
2061
+
2062
+ if (!defined $targetGroup)
2063
+ { $targetGroup = $menu; };
2064
+ };
2065
+
2066
+ $targetGroup->MarkEndOfOriginal();
2067
+ $targetGroup->PushToGroup( NaturalDocs::Menu::Entry->New(::MENU_FILE(), undef, $file, undef) );
2068
+
2069
+ $targetGroup->SetFlags( $targetGroup->Flags() | ::MENU_GROUP_UPDATETITLES() |
2070
+ ::MENU_GROUP_UPDATESTRUCTURE() | ::MENU_GROUP_UPDATEORDER() );
2071
+
2072
+ $hasChanged = 1;
2073
+ };
2074
+ };
2075
+ };
2076
+
2077
+
2078
+ #
2079
+ # Function: MatchDirectoriesAndGroups
2080
+ #
2081
+ # Determines which groups files in certain directories should be placed in.
2082
+ #
2083
+ # Returns:
2084
+ #
2085
+ # A hashref. The keys are the directory names, and the values are references to the group objects they should be placed in.
2086
+ #
2087
+ # This only repreesents directories that currently have files on the menu, so it shouldn't be assumed that every possible
2088
+ # directory will exist. To match, you should first try to match the directory, and then strip the deepest directories one by
2089
+ # one until there's a match or there's none left. If there's none left, use the root group <menu>.
2090
+ #
2091
+ sub MatchDirectoriesAndGroups
2092
+ {
2093
+ my ($self) = @_;
2094
+
2095
+ # The keys are the directory names, and the values are hashrefs. For the hashrefs, the keys are the group objects, and the
2096
+ # values are the number of files in them from that directory. In other words,
2097
+ # $directories{$directory}->{$groupEntry} = $count;
2098
+ my %directories;
2099
+ # Note that we need to use Tie::RefHash to use references as keys. Won't work otherwise. Also, not every Perl distro comes
2100
+ # with Tie::RefHash::Nestable, so we can't rely on that.
2101
+
2102
+ # We're using an index instead of pushing and popping because we want to save a list of the groups in the order they appear
2103
+ # to break ties.
2104
+ my @groups = ( $menu );
2105
+ my $groupIndex = 0;
2106
+
2107
+
2108
+ # Count the number of files in each group that appear in each directory.
2109
+
2110
+ while ($groupIndex < scalar @groups)
2111
+ {
2112
+ my $groupEntry = $groups[$groupIndex];
2113
+
2114
+ foreach my $entry (@{$groupEntry->GroupContent()})
2115
+ {
2116
+ if ($entry->Type() == ::MENU_GROUP())
2117
+ {
2118
+ push @groups, $entry;
2119
+ }
2120
+ elsif ($entry->Type() == ::MENU_FILE())
2121
+ {
2122
+ my $directory = (NaturalDocs::File->SplitPath($entry->Target()))[1];
2123
+
2124
+ if (!exists $directories{$directory})
2125
+ {
2126
+ my $subHash = { };
2127
+ tie %$subHash, 'Tie::RefHash';
2128
+ $directories{$directory} = $subHash;
2129
+ };
2130
+
2131
+ if (!exists $directories{$directory}->{$groupEntry})
2132
+ { $directories{$directory}->{$groupEntry} = 1; }
2133
+ else
2134
+ { $directories{$directory}->{$groupEntry}++; };
2135
+ };
2136
+ };
2137
+
2138
+ $groupIndex++;
2139
+ };
2140
+
2141
+
2142
+ # Determine which group goes with which directory, breaking ties by using whichever group appears first.
2143
+
2144
+ my $finalDirectories = { };
2145
+
2146
+ while (my ($directory, $directoryGroups) = each %directories)
2147
+ {
2148
+ my $bestGroup;
2149
+ my $bestCount = 0;
2150
+ my %tiedGroups; # Existence hash
2151
+
2152
+ while (my ($group, $count) = each %$directoryGroups)
2153
+ {
2154
+ if ($count > $bestCount)
2155
+ {
2156
+ $bestGroup = $group;
2157
+ $bestCount = $count;
2158
+ %tiedGroups = ( );
2159
+ }
2160
+ elsif ($count == $bestCount)
2161
+ {
2162
+ $tiedGroups{$group} = 1;
2163
+ };
2164
+ };
2165
+
2166
+ # Break ties.
2167
+ if (scalar keys %tiedGroups)
2168
+ {
2169
+ $tiedGroups{$bestGroup} = 1;
2170
+
2171
+ foreach my $group (@groups)
2172
+ {
2173
+ if (exists $tiedGroups{$group})
2174
+ {
2175
+ $bestGroup = $group;
2176
+ last;
2177
+ };
2178
+ };
2179
+ };
2180
+
2181
+
2182
+ $finalDirectories->{$directory} = $bestGroup;
2183
+ };
2184
+
2185
+
2186
+ return $finalDirectories;
2187
+ };
2188
+
2189
+
2190
+ #
2191
+ # Function: RemoveDeadFiles
2192
+ #
2193
+ # Removes files from the menu that no longer exist or no longer have Natural Docs content.
2194
+ #
2195
+ # Returns:
2196
+ #
2197
+ # The number of file entries removed.
2198
+ #
2199
+ sub RemoveDeadFiles
2200
+ {
2201
+ my ($self) = @_;
2202
+
2203
+ my @groupStack = ( $menu );
2204
+ my $numberRemoved = 0;
2205
+
2206
+ my $filesWithContent = NaturalDocs::Project->FilesWithContent();
2207
+
2208
+ while (scalar @groupStack)
2209
+ {
2210
+ my $groupEntry = pop @groupStack;
2211
+ my $groupContent = $groupEntry->GroupContent();
2212
+
2213
+ my $index = 0;
2214
+ while ($index < scalar @$groupContent)
2215
+ {
2216
+ if ($groupContent->[$index]->Type() == ::MENU_FILE() &&
2217
+ !exists $filesWithContent->{ $groupContent->[$index]->Target() } )
2218
+ {
2219
+ $groupEntry->DeleteFromGroup($index);
2220
+
2221
+ $groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() |
2222
+ ::MENU_GROUP_UPDATESTRUCTURE() );
2223
+ $numberRemoved++;
2224
+ $hasChanged = 1;
2225
+ }
2226
+
2227
+ elsif ($groupContent->[$index]->Type() == ::MENU_GROUP())
2228
+ {
2229
+ push @groupStack, $groupContent->[$index];
2230
+ $index++;
2231
+ }
2232
+
2233
+ else
2234
+ { $index++; };
2235
+ };
2236
+ };
2237
+
2238
+ return $numberRemoved;
2239
+ };
2240
+
2241
+
2242
+ #
2243
+ # Function: BanAndUnbanIndexes
2244
+ #
2245
+ # Adjusts the indexes that are banned depending on if the user added or deleted any.
2246
+ #
2247
+ sub BanAndUnbanIndexes
2248
+ {
2249
+ my ($self) = @_;
2250
+
2251
+ # Unban any indexes that are present, meaning the user added them back manually without deleting the ban.
2252
+ foreach my $index (keys %indexes)
2253
+ { delete $bannedIndexes{$index}; };
2254
+
2255
+ # Ban any indexes that were in the previous menu but not the current, meaning the user manually deleted them. However,
2256
+ # don't do this if the topic isn't indexable, meaning they changed the topic type rather than the menu.
2257
+ foreach my $index (keys %previousIndexes)
2258
+ {
2259
+ if (!exists $indexes{$index} && NaturalDocs::Topics->TypeInfo($index)->Index())
2260
+ { $bannedIndexes{$index} = 1; };
2261
+ };
2262
+ };
2263
+
2264
+
2265
+ #
2266
+ # Function: AddAndRemoveIndexes
2267
+ #
2268
+ # Automatically adds and removes index entries on the menu as necessary. <DetectIndexGroups()> should be called
2269
+ # beforehand.
2270
+ #
2271
+ sub AddAndRemoveIndexes
2272
+ {
2273
+ my ($self) = @_;
2274
+
2275
+ my %validIndexes;
2276
+ my @allIndexes = NaturalDocs::Topics->AllIndexableTypes();
2277
+
2278
+ foreach my $index (@allIndexes)
2279
+ {
2280
+ # Strip the banned indexes first so it's potentially less work for SymbolTable.
2281
+ if (!exists $bannedIndexes{$index})
2282
+ { $validIndexes{$index} = 1; };
2283
+ };
2284
+
2285
+ %validIndexes = %{NaturalDocs::SymbolTable->HasIndexes(\%validIndexes)};
2286
+
2287
+
2288
+ # Delete dead indexes and find the best index group.
2289
+
2290
+ my @groupStack = ( $menu );
2291
+
2292
+ my $bestIndexGroup;
2293
+ my $bestIndexCount = 0;
2294
+
2295
+ while (scalar @groupStack)
2296
+ {
2297
+ my $currentGroup = pop @groupStack;
2298
+ my $index = 0;
2299
+
2300
+ my $currentIndexCount = 0;
2301
+
2302
+ while ($index < scalar @{$currentGroup->GroupContent()})
2303
+ {
2304
+ my $entry = $currentGroup->GroupContent()->[$index];
2305
+
2306
+ if ($entry->Type() == ::MENU_INDEX())
2307
+ {
2308
+ $currentIndexCount++;
2309
+
2310
+ if ($currentIndexCount > $bestIndexCount)
2311
+ {
2312
+ $bestIndexCount = $currentIndexCount;
2313
+ $bestIndexGroup = $currentGroup;
2314
+ };
2315
+
2316
+ # Remove it if it's dead.
2317
+
2318
+ if (!exists $validIndexes{ $entry->Target() })
2319
+ {
2320
+ $currentGroup->DeleteFromGroup($index);
2321
+ delete $indexes{ $entry->Target() };
2322
+ $hasChanged = 1;
2323
+ }
2324
+ else
2325
+ { $index++; };
2326
+ }
2327
+
2328
+ else
2329
+ {
2330
+ if ($entry->Type() == ::MENU_GROUP())
2331
+ { push @groupStack, $entry; };
2332
+
2333
+ $index++;
2334
+ };
2335
+ };
2336
+ };
2337
+
2338
+
2339
+ # Now add the new indexes.
2340
+
2341
+ foreach my $index (keys %indexes)
2342
+ { delete $validIndexes{$index}; };
2343
+
2344
+ if (scalar keys %validIndexes)
2345
+ {
2346
+ # Add a group if there are no indexes at all.
2347
+
2348
+ if ($bestIndexCount == 0)
2349
+ {
2350
+ $menu->MarkEndOfOriginal();
2351
+
2352
+ my $newIndexGroup = NaturalDocs::Menu::Entry->New(::MENU_GROUP(), 'Index', undef,
2353
+ ::MENU_GROUP_ISINDEXGROUP());
2354
+ $menu->PushToGroup($newIndexGroup);
2355
+
2356
+ $bestIndexGroup = $newIndexGroup;
2357
+ $menu->SetFlags( $menu->Flags() | ::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() );
2358
+ };
2359
+
2360
+ # Add the new indexes.
2361
+
2362
+ $bestIndexGroup->MarkEndOfOriginal();
2363
+ my $isIndexGroup = $bestIndexGroup->Flags() & ::MENU_GROUP_ISINDEXGROUP();
2364
+
2365
+ foreach my $index (keys %validIndexes)
2366
+ {
2367
+ my $title;
2368
+
2369
+ if ($isIndexGroup)
2370
+ {
2371
+ if ($index eq ::TOPIC_GENERAL())
2372
+ { $title = 'Everything'; }
2373
+ else
2374
+ { $title = NaturalDocs::Topics->NameOfType($index, 1); };
2375
+ }
2376
+ else
2377
+ {
2378
+ $title = NaturalDocs::Topics->NameOfType($index) . ' Index';
2379
+ };
2380
+
2381
+ my $newEntry = NaturalDocs::Menu::Entry->New(::MENU_INDEX(), $title, $index, undef);
2382
+ $bestIndexGroup->PushToGroup($newEntry);
2383
+
2384
+ $indexes{$index} = 1;
2385
+ };
2386
+
2387
+ $bestIndexGroup->SetFlags( $bestIndexGroup->Flags() |
2388
+ ::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() );
2389
+ $hasChanged = 1;
2390
+ };
2391
+ };
2392
+
2393
+
2394
+ #
2395
+ # Function: RemoveDeadGroups
2396
+ #
2397
+ # Removes groups with less than two entries. It will always remove empty groups, and it will remove groups with one entry if it
2398
+ # has the <MENU_GROUP_UPDATESTRUCTURE> flag.
2399
+ #
2400
+ sub RemoveDeadGroups
2401
+ {
2402
+ my ($self) = @_;
2403
+
2404
+ my $index = 0;
2405
+
2406
+ while ($index < scalar @{$menu->GroupContent()})
2407
+ {
2408
+ my $entry = $menu->GroupContent()->[$index];
2409
+
2410
+ if ($entry->Type() == ::MENU_GROUP())
2411
+ {
2412
+ my $removed = $self->RemoveIfDead($entry, $menu, $index);
2413
+
2414
+ if (!$removed)
2415
+ { $index++; };
2416
+ }
2417
+ else
2418
+ { $index++; };
2419
+ };
2420
+ };
2421
+
2422
+
2423
+ #
2424
+ # Function: RemoveIfDead
2425
+ #
2426
+ # Checks a group and all its sub-groups for life and remove any that are dead. Empty groups are removed, and groups with one
2427
+ # entry and the <MENU_GROUP_UPDATESTRUCTURE> flag have their entry moved to the parent group.
2428
+ #
2429
+ # Parameters:
2430
+ #
2431
+ # groupEntry - The group to check for possible deletion.
2432
+ # parentGroupEntry - The parent group to move the single entry to if necessary.
2433
+ # parentGroupIndex - The index of the group in its parent.
2434
+ #
2435
+ # Returns:
2436
+ #
2437
+ # Whether the group was removed or not.
2438
+ #
2439
+ sub RemoveIfDead #(groupEntry, parentGroupEntry, parentGroupIndex)
2440
+ {
2441
+ my ($self, $groupEntry, $parentGroupEntry, $parentGroupIndex) = @_;
2442
+
2443
+
2444
+ # Do all sub-groups first, since their deletions will affect our UPDATESTRUCTURE flag and content count.
2445
+
2446
+ my $index = 0;
2447
+ while ($index < scalar @{$groupEntry->GroupContent()})
2448
+ {
2449
+ my $entry = $groupEntry->GroupContent()->[$index];
2450
+
2451
+ if ($entry->Type() == ::MENU_GROUP())
2452
+ {
2453
+ my $removed = $self->RemoveIfDead($entry, $groupEntry, $index);
2454
+
2455
+ if (!$removed)
2456
+ { $index++; };
2457
+ }
2458
+ else
2459
+ { $index++; };
2460
+ };
2461
+
2462
+
2463
+ # Now check ourself.
2464
+
2465
+ my $count = scalar @{$groupEntry->GroupContent()};
2466
+ if ($groupEntry->Flags() & ::MENU_GROUP_HASENDOFORIGINAL())
2467
+ { $count--; };
2468
+
2469
+ if ($count == 0)
2470
+ {
2471
+ $parentGroupEntry->DeleteFromGroup($parentGroupIndex);
2472
+
2473
+ $parentGroupEntry->SetFlags( $parentGroupEntry->Flags() | ::MENU_GROUP_UPDATESTRUCTURE() );
2474
+
2475
+ $hasChanged = 1;
2476
+ return 1;
2477
+ }
2478
+ elsif ($count == 1 && ($groupEntry->Flags() & ::MENU_GROUP_UPDATESTRUCTURE()) )
2479
+ {
2480
+ my $onlyEntry = $groupEntry->GroupContent()->[0];
2481
+ if ($onlyEntry->Type() == ::MENU_ENDOFORIGINAL())
2482
+ { $onlyEntry = $groupEntry->GroupContent()->[1]; };
2483
+
2484
+ $parentGroupEntry->DeleteFromGroup($parentGroupIndex);
2485
+
2486
+ $parentGroupEntry->MarkEndOfOriginal();
2487
+ $parentGroupEntry->PushToGroup($onlyEntry);
2488
+
2489
+ $parentGroupEntry->SetFlags( $parentGroupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() |
2490
+ ::MENU_GROUP_UPDATEORDER() | ::MENU_GROUP_UPDATESTRUCTURE() );
2491
+
2492
+ $hasChanged = 1;
2493
+ return 1;
2494
+ }
2495
+ else
2496
+ { return undef; };
2497
+ };
2498
+
2499
+
2500
+ #
2501
+ # Function: DetectIndexGroups
2502
+ #
2503
+ # Finds groups that are primarily used for indexes and gives them the <MENU_GROUP_ISINDEXGROUP> flag.
2504
+ #
2505
+ sub DetectIndexGroups
2506
+ {
2507
+ my ($self) = @_;
2508
+
2509
+ my @groupStack = ( $menu );
2510
+
2511
+ while (scalar @groupStack)
2512
+ {
2513
+ my $groupEntry = pop @groupStack;
2514
+
2515
+ my $isIndexGroup = -1; # -1: Can't tell yet. 0: Can't be an index group. 1: Is an index group so far.
2516
+
2517
+ foreach my $entry (@{$groupEntry->GroupContent()})
2518
+ {
2519
+ if ($entry->Type() == ::MENU_INDEX())
2520
+ {
2521
+ if ($isIndexGroup == -1)
2522
+ { $isIndexGroup = 1; };
2523
+ }
2524
+
2525
+ # Text is tolerated, but it still needs at least one index entry.
2526
+ elsif ($entry->Type() != ::MENU_TEXT())
2527
+ {
2528
+ $isIndexGroup = 0;
2529
+
2530
+ if ($entry->Type() == ::MENU_GROUP())
2531
+ { push @groupStack, $entry; };
2532
+ };
2533
+ };
2534
+
2535
+ if ($isIndexGroup == 1)
2536
+ {
2537
+ $groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_ISINDEXGROUP() );
2538
+ };
2539
+ };
2540
+ };
2541
+
2542
+
2543
+ #
2544
+ # Function: CreateDirectorySubGroups
2545
+ #
2546
+ # Where possible, creates sub-groups based on directories for any long groups that have <MENU_GROUP_UPDATESTRUCTURE>
2547
+ # set. Clears the flag afterwards on groups that are short enough to not need any more sub-groups, but leaves it for the rest.
2548
+ #
2549
+ sub CreateDirectorySubGroups
2550
+ {
2551
+ my ($self) = @_;
2552
+
2553
+ my @groupStack = ( $menu );
2554
+
2555
+ foreach my $groupEntry (@groupStack)
2556
+ {
2557
+ if ($groupEntry->Flags() & ::MENU_GROUP_UPDATESTRUCTURE())
2558
+ {
2559
+ # Count the number of files.
2560
+
2561
+ my $fileCount = 0;
2562
+
2563
+ foreach my $entry (@{$groupEntry->GroupContent()})
2564
+ {
2565
+ if ($entry->Type() == ::MENU_FILE())
2566
+ { $fileCount++; };
2567
+ };
2568
+
2569
+
2570
+ if ($fileCount > MAXFILESINGROUP)
2571
+ {
2572
+ my @sharedDirectories = $self->SharedDirectoriesOf($groupEntry);
2573
+ my $unsharedIndex = scalar @sharedDirectories;
2574
+
2575
+ # The keys are the first directory entries after the shared ones, and the values are the number of files that are in
2576
+ # that directory. Files that don't have subdirectories after the shared directories aren't included because they shouldn't
2577
+ # be put in a subgroup.
2578
+ my %directoryCounts;
2579
+
2580
+ foreach my $entry (@{$groupEntry->GroupContent()})
2581
+ {
2582
+ if ($entry->Type() == ::MENU_FILE())
2583
+ {
2584
+ my @entryDirectories = NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] );
2585
+
2586
+ if (scalar @entryDirectories > $unsharedIndex)
2587
+ {
2588
+ my $unsharedDirectory = $entryDirectories[$unsharedIndex];
2589
+
2590
+ if (!exists $directoryCounts{$unsharedDirectory})
2591
+ { $directoryCounts{$unsharedDirectory} = 1; }
2592
+ else
2593
+ { $directoryCounts{$unsharedDirectory}++; };
2594
+ };
2595
+ };
2596
+ };
2597
+
2598
+
2599
+ # Now create the subgroups.
2600
+
2601
+ # The keys are the first directory entries after the shared ones, and the values are the groups for those files to be
2602
+ # put in. There will only be entries for the groups with at least MINFILESINNEWGROUP files.
2603
+ my %directoryGroups;
2604
+
2605
+ while (my ($directory, $count) = each %directoryCounts)
2606
+ {
2607
+ if ($count >= MINFILESINNEWGROUP)
2608
+ {
2609
+ my $newGroup = NaturalDocs::Menu::Entry->New( ::MENU_GROUP(), ucfirst($directory), undef,
2610
+ ::MENU_GROUP_UPDATETITLES() |
2611
+ ::MENU_GROUP_UPDATEORDER() );
2612
+
2613
+ if ($count > MAXFILESINGROUP)
2614
+ { $newGroup->SetFlags( $newGroup->Flags() | ::MENU_GROUP_UPDATESTRUCTURE()); };
2615
+
2616
+ $groupEntry->MarkEndOfOriginal();
2617
+ push @{$groupEntry->GroupContent()}, $newGroup;
2618
+
2619
+ $directoryGroups{$directory} = $newGroup;
2620
+ $fileCount -= $count;
2621
+ };
2622
+ };
2623
+
2624
+
2625
+ # Now fill the subgroups.
2626
+
2627
+ if (scalar keys %directoryGroups)
2628
+ {
2629
+ my $afterOriginal;
2630
+ my $index = 0;
2631
+
2632
+ while ($index < scalar @{$groupEntry->GroupContent()})
2633
+ {
2634
+ my $entry = $groupEntry->GroupContent()->[$index];
2635
+
2636
+ if ($entry->Type() == ::MENU_FILE())
2637
+ {
2638
+ my @entryDirectories =
2639
+ NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] );
2640
+
2641
+ my $unsharedDirectory = $entryDirectories[$unsharedIndex];
2642
+
2643
+ if (exists $directoryGroups{$unsharedDirectory})
2644
+ {
2645
+ my $targetGroup = $directoryGroups{$unsharedDirectory};
2646
+
2647
+ if ($afterOriginal)
2648
+ { $targetGroup->MarkEndOfOriginal(); };
2649
+ $targetGroup->PushToGroup($entry);
2650
+
2651
+ $groupEntry->DeleteFromGroup($index);
2652
+ }
2653
+ else
2654
+ { $index++; };
2655
+ }
2656
+
2657
+ elsif ($entry->Type() == ::MENU_ENDOFORIGINAL())
2658
+ {
2659
+ $afterOriginal = 1;
2660
+ $index++;
2661
+ }
2662
+
2663
+ elsif ($entry->Type() == ::MENU_GROUP())
2664
+ {
2665
+ # See if we need to relocate this group.
2666
+
2667
+ my @groupDirectories = $self->SharedDirectoriesOf($entry);
2668
+
2669
+ # The group's shared directories must be at least two levels deeper than the current. If the first level deeper
2670
+ # is a new group, move it there because it's a subdirectory of that one.
2671
+ if (scalar @groupDirectories - scalar @sharedDirectories >= 2)
2672
+ {
2673
+ my $unsharedDirectory = $groupDirectories[$unsharedIndex];
2674
+
2675
+ if (exists $directoryGroups{$unsharedDirectory} &&
2676
+ $directoryGroups{$unsharedDirectory} != $entry)
2677
+ {
2678
+ my $targetGroup = $directoryGroups{$unsharedDirectory};
2679
+
2680
+ if ($afterOriginal)
2681
+ { $targetGroup->MarkEndOfOriginal(); };
2682
+ $targetGroup->PushToGroup($entry);
2683
+
2684
+ $groupEntry->DeleteFromGroup($index);
2685
+
2686
+ # We need to retitle the group if it has the name of the unshared directory.
2687
+
2688
+ my $oldTitle = $entry->Title();
2689
+ $oldTitle =~ s/ +//g;
2690
+ $unsharedDirectory =~ s/ +//g;
2691
+
2692
+ if (lc($oldTitle) eq lc($unsharedDirectory))
2693
+ {
2694
+ $entry->SetTitle($groupDirectories[$unsharedIndex + 1]);
2695
+ };
2696
+ }
2697
+ else
2698
+ { $index++; };
2699
+ }
2700
+ else
2701
+ { $index++; };
2702
+ }
2703
+
2704
+ else
2705
+ { $index++; };
2706
+ };
2707
+
2708
+ $hasChanged = 1;
2709
+
2710
+ if ($fileCount <= MAXFILESINGROUP)
2711
+ { $groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_UPDATESTRUCTURE() ); };
2712
+
2713
+ $groupEntry->SetFlags( $groupEntry->Flags() | ::MENU_GROUP_UPDATETITLES() |
2714
+ ::MENU_GROUP_UPDATEORDER() );
2715
+ };
2716
+
2717
+ }; # If group has >MAXFILESINGROUP files
2718
+ }; # If group has UPDATESTRUCTURE
2719
+
2720
+
2721
+ # Okay, now go through all the subgroups. We do this after the above so that newly created groups can get subgrouped
2722
+ # further.
2723
+
2724
+ foreach my $entry (@{$groupEntry->GroupContent()})
2725
+ {
2726
+ if ($entry->Type() == ::MENU_GROUP())
2727
+ { push @groupStack, $entry; };
2728
+ };
2729
+
2730
+ }; # For each group entry
2731
+ };
2732
+
2733
+
2734
+ #
2735
+ # Function: DetectOrder
2736
+ #
2737
+ # Detects the order of the entries in all groups that have the <MENU_GROUP_UPDATEORDER> flag set. Will set one of the
2738
+ # <MENU_GROUP_FILESSORTED>, <MENU_GROUP_FILESANDGROUPSSORTED>, <MENU_GROUP_EVERYTHINGSORTED>, or
2739
+ # <MENU_GROUP_UNSORTED> flags. It will always go for the most comprehensive sort possible, so if a group only has one
2740
+ # entry, it will be flagged as <MENU_GROUP_EVERYTHINGSORTED>.
2741
+ #
2742
+ # <DetectIndexGroups()> should be called beforehand, as the <MENU_GROUP_ISINDEXGROUP> flag affects how the order is
2743
+ # detected.
2744
+ #
2745
+ # The sort detection stops if it reaches a <MENU_ENDOFORIGINAL> entry, so new entries can be added to the end while still
2746
+ # allowing the original sort to be detected.
2747
+ #
2748
+ # Parameters:
2749
+ #
2750
+ # forceAll - If set, the order will be detected for all groups regardless of whether <MENU_GROUP_UPDATEORDER> is set.
2751
+ #
2752
+ sub DetectOrder #(forceAll)
2753
+ {
2754
+ my ($self, $forceAll) = @_;
2755
+ my @groupStack = ( $menu );
2756
+
2757
+ while (scalar @groupStack)
2758
+ {
2759
+ my $groupEntry = pop @groupStack;
2760
+ my $index = 0;
2761
+
2762
+
2763
+ # First detect the sort.
2764
+
2765
+ if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATEORDER()) )
2766
+ {
2767
+ my $order = ::MENU_GROUP_EVERYTHINGSORTED();
2768
+
2769
+ my $lastFile;
2770
+ my $lastFileOrGroup;
2771
+
2772
+ while ($index < scalar @{$groupEntry->GroupContent()} &&
2773
+ $groupEntry->GroupContent()->[$index]->Type() != ::MENU_ENDOFORIGINAL() &&
2774
+ $order != ::MENU_GROUP_UNSORTED())
2775
+ {
2776
+ my $entry = $groupEntry->GroupContent()->[$index];
2777
+
2778
+
2779
+ # Ignore the last entry if it's an index group. We don't want it to affect the sort.
2780
+
2781
+ if ($index + 1 == scalar @{$groupEntry->GroupContent()} &&
2782
+ $entry->Type() == ::MENU_GROUP() && ($entry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) )
2783
+ {
2784
+ # Ignore.
2785
+
2786
+ # This is an awkward code construct, basically working towards an else instead of using an if, but the code just gets
2787
+ # too hard to read otherwise. The compiled code should work out to roughly the same thing anyway.
2788
+ }
2789
+
2790
+
2791
+ # Ignore the first entry if it's the general index in an index group. We don't want it to affect the sort.
2792
+
2793
+ elsif ($index == 0 && ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) &&
2794
+ $entry->Type() == ::MENU_INDEX() && $entry->Target() eq ::TOPIC_GENERAL() )
2795
+ {
2796
+ # Ignore.
2797
+ }
2798
+
2799
+
2800
+ # Degenerate the sort.
2801
+
2802
+ else
2803
+ {
2804
+
2805
+ if ($order == ::MENU_GROUP_EVERYTHINGSORTED() && $index > 0 &&
2806
+ ::StringCompare($entry->Title(), $groupEntry->GroupContent()->[$index - 1]->Title()) < 0)
2807
+ { $order = ::MENU_GROUP_FILESANDGROUPSSORTED(); };
2808
+
2809
+ if ($order == ::MENU_GROUP_FILESANDGROUPSSORTED() &&
2810
+ ($entry->Type() == ::MENU_FILE() || $entry->Type() == ::MENU_GROUP()) &&
2811
+ defined $lastFileOrGroup && ::StringCompare($entry->Title(), $lastFileOrGroup->Title()) < 0)
2812
+ { $order = ::MENU_GROUP_FILESSORTED(); };
2813
+
2814
+ if ($order == ::MENU_GROUP_FILESSORTED() &&
2815
+ $entry->Type() == ::MENU_FILE() && defined $lastFile &&
2816
+ ::StringCompare($entry->Title(), $lastFile->Title()) < 0)
2817
+ { $order = ::MENU_GROUP_UNSORTED(); };
2818
+
2819
+ };
2820
+
2821
+
2822
+ # Set the lastX parameters for comparison and add sub-groups to the stack.
2823
+
2824
+ if ($entry->Type() == ::MENU_FILE())
2825
+ {
2826
+ $lastFile = $entry;
2827
+ $lastFileOrGroup = $entry;
2828
+ }
2829
+ elsif ($entry->Type() == ::MENU_GROUP())
2830
+ {
2831
+ $lastFileOrGroup = $entry;
2832
+ push @groupStack, $entry;
2833
+ };
2834
+
2835
+ $index++;
2836
+ };
2837
+
2838
+ $groupEntry->SetFlags($groupEntry->Flags() | $order);
2839
+ };
2840
+
2841
+
2842
+ # Find any subgroups in the remaining entries.
2843
+
2844
+ while ($index < scalar @{$groupEntry->GroupContent()})
2845
+ {
2846
+ my $entry = $groupEntry->GroupContent()->[$index];
2847
+
2848
+ if ($entry->Type() == ::MENU_GROUP())
2849
+ { push @groupStack, $entry; };
2850
+
2851
+ $index++;
2852
+ };
2853
+ };
2854
+ };
2855
+
2856
+
2857
+ #
2858
+ # Function: GenerateAutoFileTitles
2859
+ #
2860
+ # Creates titles for the unlocked file entries in all groups that have the <MENU_GROUP_UPDATETITLES> flag set. It clears the
2861
+ # flag afterwards so it can be used efficiently for multiple sweeps.
2862
+ #
2863
+ # Parameters:
2864
+ #
2865
+ # forceAll - If set, forces all the unlocked file titles to update regardless of whether the group has the
2866
+ # <MENU_GROUP_UPDATETITLES> flag set.
2867
+ #
2868
+ sub GenerateAutoFileTitles #(forceAll)
2869
+ {
2870
+ my ($self, $forceAll) = @_;
2871
+
2872
+ my @groupStack = ( $menu );
2873
+
2874
+ while (scalar @groupStack)
2875
+ {
2876
+ my $groupEntry = pop @groupStack;
2877
+
2878
+ if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATETITLES()) )
2879
+ {
2880
+ # Find common prefixes and paths to strip from the default menu titles.
2881
+
2882
+ my @sharedDirectories = $self->SharedDirectoriesOf($groupEntry);
2883
+ my $noSharedDirectories = (scalar @sharedDirectories == 0);
2884
+
2885
+ my @sharedPrefixes;
2886
+ my $noSharedPrefixes;
2887
+
2888
+ foreach my $entry (@{$groupEntry->GroupContent()})
2889
+ {
2890
+ if ($entry->Type() == ::MENU_FILE())
2891
+ {
2892
+ # Find the common prefixes among all file entries that are unlocked and don't use the file name as their default title.
2893
+
2894
+ my $defaultTitle = NaturalDocs::Project->DefaultMenuTitleOf($entry->Target());
2895
+
2896
+ if (!$noSharedPrefixes && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0 &&
2897
+ $defaultTitle ne $entry->Target())
2898
+ {
2899
+ # If the filename is part of the title, separate it off so no part of it gets included as a common prefix. This would
2900
+ # happen if there's a group with only one file in it (Project.h => h) or only files that differ by extension
2901
+ # (Project.h, Project.cpp => h, cpp) and people labeled them manually (// File: Project.h).
2902
+ my $filename = (NaturalDocs::File->SplitPath($entry->Target()))[2];
2903
+ my $filenamePart;
2904
+
2905
+ if ( length $defaultTitle >= length $filename &&
2906
+ lc(substr($defaultTitle, 0 - length($filename))) eq lc($filename) )
2907
+ {
2908
+ $filenamePart = substr($defaultTitle, 0 - length($filename));
2909
+ $defaultTitle = substr($defaultTitle, 0, 0 - length($filename));
2910
+ };
2911
+
2912
+
2913
+ my @entryPrefixes = split(/(\.|::|->)/, $defaultTitle);
2914
+
2915
+ # Remove potential leading undef/empty string.
2916
+ if (!length $entryPrefixes[0])
2917
+ { shift @entryPrefixes; };
2918
+
2919
+ # Remove last entry. Something has to exist for the title. If we already separated off the filename, that will be
2920
+ # it instead.
2921
+ if (!$filenamePart)
2922
+ { pop @entryPrefixes; };
2923
+
2924
+ if (!scalar @entryPrefixes)
2925
+ { $noSharedPrefixes = 1; }
2926
+ elsif (!scalar @sharedPrefixes)
2927
+ { @sharedPrefixes = @entryPrefixes; }
2928
+ elsif ($entryPrefixes[0] ne $sharedPrefixes[0])
2929
+ { $noSharedPrefixes = 1; }
2930
+
2931
+ # If both arrays have entries, and the first is shared...
2932
+ else
2933
+ {
2934
+ my $index = 1;
2935
+
2936
+ while ($index < scalar @sharedPrefixes && $entryPrefixes[$index] eq $sharedPrefixes[$index])
2937
+ { $index++; };
2938
+
2939
+ if ($index < scalar @sharedPrefixes)
2940
+ { splice(@sharedPrefixes, $index); };
2941
+ };
2942
+ };
2943
+
2944
+ }; # if entry is MENU_FILE
2945
+ }; # foreach entry in group content.
2946
+
2947
+
2948
+ if (!scalar @sharedPrefixes)
2949
+ { $noSharedPrefixes = 1; };
2950
+
2951
+
2952
+ # Update all the menu titles of unlocked file entries.
2953
+
2954
+ foreach my $entry (@{$groupEntry->GroupContent()})
2955
+ {
2956
+ if ($entry->Type() == ::MENU_FILE() && ($entry->Flags() & ::MENU_FILE_NOAUTOTITLE()) == 0)
2957
+ {
2958
+ my $title = NaturalDocs::Project->DefaultMenuTitleOf($entry->Target());
2959
+
2960
+ if ($title eq $entry->Target())
2961
+ {
2962
+ my ($volume, $directoryString, $file) = NaturalDocs::File->SplitPath($entry->Target());
2963
+ my @directories = NaturalDocs::File->SplitDirectories($directoryString);
2964
+
2965
+ if (!$noSharedDirectories)
2966
+ { splice(@directories, 0, scalar @sharedDirectories); };
2967
+
2968
+ # directory\...\directory\file.ext
2969
+
2970
+ if (scalar @directories > 2)
2971
+ { @directories = ( $directories[0], '...', $directories[-1] ); };
2972
+
2973
+ $directoryString = NaturalDocs::File->JoinDirectories(@directories);
2974
+ $title = NaturalDocs::File->JoinPaths($directoryString, $file);
2975
+ }
2976
+
2977
+ else
2978
+ {
2979
+ my $filename = (NaturalDocs::File->SplitPath($entry->Target()))[2];
2980
+ my $filenamePart;
2981
+
2982
+ if ( length $title >= length $filename &&
2983
+ lc(substr($title, 0 - length($filename))) eq lc($filename) )
2984
+ {
2985
+ $filenamePart = substr($title, 0 - length($filename));
2986
+ $title = substr($title, 0, 0 - length($filename));
2987
+ };
2988
+
2989
+ my @segments = split(/(::|\.|->)/, $title);
2990
+ if (!length $segments[0])
2991
+ { shift @segments; };
2992
+
2993
+ if ($filenamePart)
2994
+ { push @segments, $filenamePart; };
2995
+
2996
+ if (!$noSharedPrefixes)
2997
+ { splice(@segments, 0, scalar @sharedPrefixes); };
2998
+
2999
+ # package...package::target
3000
+
3001
+ if (scalar @segments > 5)
3002
+ { splice(@segments, 1, scalar @segments - 4, '...'); };
3003
+
3004
+ $title = join('', @segments);
3005
+ };
3006
+
3007
+ $entry->SetTitle($title);
3008
+ }; # If entry is an unlocked file
3009
+ }; # Foreach entry
3010
+
3011
+ $groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_UPDATETITLES() );
3012
+
3013
+ }; # If updating group titles
3014
+
3015
+ # Now find any subgroups.
3016
+ foreach my $entry (@{$groupEntry->GroupContent()})
3017
+ {
3018
+ if ($entry->Type() == ::MENU_GROUP())
3019
+ { push @groupStack, $entry; };
3020
+ };
3021
+ };
3022
+
3023
+ };
3024
+
3025
+
3026
+ #
3027
+ # Function: ResortGroups
3028
+ #
3029
+ # Resorts all groups that have <MENU_GROUP_UPDATEORDER> set. Assumes <DetectOrder()> and <GenerateAutoFileTitles()>
3030
+ # have already been called. Will clear the flag and any <MENU_ENDOFORIGINAL> entries on reordered groups.
3031
+ #
3032
+ # Parameters:
3033
+ #
3034
+ # forceAll - If set, resorts all groups regardless of whether <MENU_GROUP_UPDATEORDER> is set.
3035
+ #
3036
+ sub ResortGroups #(forceAll)
3037
+ {
3038
+ my ($self, $forceAll) = @_;
3039
+ my @groupStack = ( $menu );
3040
+
3041
+ while (scalar @groupStack)
3042
+ {
3043
+ my $groupEntry = pop @groupStack;
3044
+
3045
+ if ($forceAll || ($groupEntry->Flags() & ::MENU_GROUP_UPDATEORDER()) )
3046
+ {
3047
+ my $newEntriesIndex;
3048
+
3049
+
3050
+ # Strip the ENDOFORIGINAL.
3051
+
3052
+ if ($groupEntry->Flags() & ::MENU_GROUP_HASENDOFORIGINAL())
3053
+ {
3054
+ $newEntriesIndex = 0;
3055
+
3056
+ while ($newEntriesIndex < scalar @{$groupEntry->GroupContent()} &&
3057
+ $groupEntry->GroupContent()->[$newEntriesIndex]->Type() != ::MENU_ENDOFORIGINAL() )
3058
+ { $newEntriesIndex++; };
3059
+
3060
+ $groupEntry->DeleteFromGroup($newEntriesIndex);
3061
+
3062
+ $groupEntry->SetFlags( $groupEntry->Flags() & ~::MENU_GROUP_HASENDOFORIGINAL() );
3063
+ }
3064
+ else
3065
+ { $newEntriesIndex = -1; };
3066
+
3067
+
3068
+ # Strip the exceptions.
3069
+
3070
+ my $trailingIndexGroup;
3071
+ my $leadingGeneralIndex;
3072
+
3073
+ if ( ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) &&
3074
+ $groupEntry->GroupContent()->[0]->Type() == ::MENU_INDEX() &&
3075
+ $groupEntry->GroupContent()->[0]->Target() eq ::TOPIC_GENERAL() )
3076
+ {
3077
+ $leadingGeneralIndex = shift @{$groupEntry->GroupContent()};
3078
+ if ($newEntriesIndex != -1)
3079
+ { $newEntriesIndex--; };
3080
+ }
3081
+
3082
+ elsif (scalar @{$groupEntry->GroupContent()} && $newEntriesIndex != 0)
3083
+ {
3084
+ my $lastIndex;
3085
+
3086
+ if ($newEntriesIndex != -1)
3087
+ { $lastIndex = $newEntriesIndex - 1; }
3088
+ else
3089
+ { $lastIndex = scalar @{$groupEntry->GroupContent()} - 1; };
3090
+
3091
+ if ($groupEntry->GroupContent()->[$lastIndex]->Type() == ::MENU_GROUP() &&
3092
+ ( $groupEntry->GroupContent()->[$lastIndex]->Flags() & ::MENU_GROUP_ISINDEXGROUP() ) )
3093
+ {
3094
+ $trailingIndexGroup = $groupEntry->GroupContent()->[$lastIndex];
3095
+ $groupEntry->DeleteFromGroup($lastIndex);
3096
+
3097
+ if ($newEntriesIndex != -1)
3098
+ { $newEntriesIndex++; };
3099
+ };
3100
+ };
3101
+
3102
+
3103
+ # If there weren't already exceptions, strip them from the new entries.
3104
+
3105
+ if ( (!defined $trailingIndexGroup || !defined $leadingGeneralIndex) && $newEntriesIndex != -1)
3106
+ {
3107
+ my $index = $newEntriesIndex;
3108
+
3109
+ while ($index < scalar @{$groupEntry->GroupContent()})
3110
+ {
3111
+ my $entry = $groupEntry->GroupContent()->[$index];
3112
+
3113
+ if (!defined $trailingIndexGroup &&
3114
+ $entry->Type() == ::MENU_GROUP() && ($entry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) )
3115
+ {
3116
+ $trailingIndexGroup = $entry;
3117
+ $groupEntry->DeleteFromGroup($index);
3118
+ }
3119
+ elsif (!defined $leadingGeneralIndex && ($groupEntry->Flags() & ::MENU_GROUP_ISINDEXGROUP()) &&
3120
+ $entry->Type() == ::MENU_INDEX() && !defined $entry->Target())
3121
+ {
3122
+ $leadingGeneralIndex = $entry;
3123
+ $groupEntry->DeleteFromGroup($index);
3124
+ }
3125
+ else
3126
+ { $index++; };
3127
+ };
3128
+ };
3129
+
3130
+
3131
+ # If there's no order, we still want to sort the new additions.
3132
+
3133
+ if ($groupEntry->Flags() & ::MENU_GROUP_UNSORTED())
3134
+ {
3135
+ if ($newEntriesIndex != -1)
3136
+ {
3137
+ my @newEntries =
3138
+ @{$groupEntry->GroupContent()}[$newEntriesIndex..scalar @{$groupEntry->GroupContent()} - 1];
3139
+
3140
+ @newEntries = sort { $self->CompareEntries($a, $b) } @newEntries;
3141
+
3142
+ foreach my $newEntry (@newEntries)
3143
+ {
3144
+ $groupEntry->GroupContent()->[$newEntriesIndex] = $newEntry;
3145
+ $newEntriesIndex++;
3146
+ };
3147
+ };
3148
+ }
3149
+
3150
+ elsif ($groupEntry->Flags() & ::MENU_GROUP_EVERYTHINGSORTED())
3151
+ {
3152
+ @{$groupEntry->GroupContent()} = sort { $self->CompareEntries($a, $b) } @{$groupEntry->GroupContent()};
3153
+ }
3154
+
3155
+ elsif ( ($groupEntry->Flags() & ::MENU_GROUP_FILESSORTED()) ||
3156
+ ($groupEntry->Flags() & ::MENU_GROUP_FILESANDGROUPSSORTED()) )
3157
+ {
3158
+ my $groupContent = $groupEntry->GroupContent();
3159
+ my @newEntries;
3160
+
3161
+ if ($newEntriesIndex != -1)
3162
+ { @newEntries = splice( @$groupContent, $newEntriesIndex ); };
3163
+
3164
+
3165
+ # First resort the existing entries.
3166
+
3167
+ # A couple of support functions. They're defined here instead of spun off into their own functions because they're only
3168
+ # used here and to make them general we would need to add support for the other sort options.
3169
+
3170
+ sub IsIncludedInSort #(groupEntry, entry)
3171
+ {
3172
+ my ($self, $groupEntry, $entry) = @_;
3173
+
3174
+ return ($entry->Type() == ::MENU_FILE() ||
3175
+ ( $entry->Type() == ::MENU_GROUP() &&
3176
+ ($groupEntry->Flags() & ::MENU_GROUP_FILESANDGROUPSSORTED()) ) );
3177
+ };
3178
+
3179
+ sub IsSorted #(groupEntry)
3180
+ {
3181
+ my ($self, $groupEntry) = @_;
3182
+ my $lastApplicable;
3183
+
3184
+ foreach my $entry (@{$groupEntry->GroupContent()})
3185
+ {
3186
+ # If the entry is applicable to the sort order...
3187
+ if ($self->IsIncludedInSort($groupEntry, $entry))
3188
+ {
3189
+ if (defined $lastApplicable)
3190
+ {
3191
+ if ($self->CompareEntries($entry, $lastApplicable) < 0)
3192
+ { return undef; };
3193
+ };
3194
+
3195
+ $lastApplicable = $entry;
3196
+ };
3197
+ };
3198
+
3199
+ return 1;
3200
+ };
3201
+
3202
+
3203
+ # There's a good chance it's still sorted. They should only become unsorted if an auto-title changes.
3204
+ if (!$self->IsSorted($groupEntry))
3205
+ {
3206
+ # Crap. Okay, method one is to sort each group of continuous sortable elements. There's a possibility that doing
3207
+ # this will cause the whole to become sorted again. We try this first, even though it isn't guaranteed to succeed,
3208
+ # because it will restore the sort without moving any unsortable entries.
3209
+
3210
+ # Copy it because we'll need the original if this fails.
3211
+ my @originalGroupContent = @$groupContent;
3212
+
3213
+ my $index = 0;
3214
+ my $startSortable = 0;
3215
+
3216
+ while (1)
3217
+ {
3218
+ # If index is on an unsortable entry or the end of the array...
3219
+ if ($index == scalar @$groupContent || !$self->IsIncludedInSort($groupEntry, $groupContent->[$index]))
3220
+ {
3221
+ # If we have at least two sortable entries...
3222
+ if ($index - $startSortable >= 2)
3223
+ {
3224
+ # Sort them.
3225
+ my @sortableEntries = @{$groupContent}[$startSortable .. $index - 1];
3226
+ @sortableEntries = sort { $self->CompareEntries($a, $b) } @sortableEntries;
3227
+ foreach my $sortableEntry (@sortableEntries)
3228
+ {
3229
+ $groupContent->[$startSortable] = $sortableEntry;
3230
+ $startSortable++;
3231
+ };
3232
+ };
3233
+
3234
+ if ($index == scalar @$groupContent)
3235
+ { last; };
3236
+
3237
+ $startSortable = $index + 1;
3238
+ };
3239
+
3240
+ $index++;
3241
+ };
3242
+
3243
+ if (!$self->IsSorted($groupEntry))
3244
+ {
3245
+ # Crap crap. Okay, now we do a full sort but with potential damage to the original structure. Each unsortable
3246
+ # element is locked to the next sortable element. We sort the sortable elements, bringing all the unsortable
3247
+ # pieces with them.
3248
+
3249
+ my @pieces = ( [ ] );
3250
+ my $currentPiece = $pieces[0];
3251
+
3252
+ foreach my $entry (@originalGroupContent)
3253
+ {
3254
+ push @$currentPiece, $entry;
3255
+
3256
+ # If the entry is sortable...
3257
+ if ($self->IsIncludedInSort($groupEntry, $entry))
3258
+ {
3259
+ $currentPiece = [ ];
3260
+ push @pieces, $currentPiece;
3261
+ };
3262
+ };
3263
+
3264
+ my $lastUnsortablePiece;
3265
+
3266
+ # If the last entry was sortable, we'll have an empty piece at the end. Drop it.
3267
+ if (scalar @{$pieces[-1]} == 0)
3268
+ { pop @pieces; }
3269
+
3270
+ # If the last entry wasn't sortable, the last piece won't end with a sortable element. Save it, but remove it
3271
+ # from the list.
3272
+ else
3273
+ { $lastUnsortablePiece = pop @pieces; };
3274
+
3275
+ # Sort the list.
3276
+ @pieces = sort { $self->CompareEntries( $a->[-1], $b->[-1] ) } @pieces;
3277
+
3278
+ # Copy it back to the original.
3279
+ if (defined $lastUnsortablePiece)
3280
+ { push @pieces, $lastUnsortablePiece; };
3281
+
3282
+ my $index = 0;
3283
+
3284
+ foreach my $piece (@pieces)
3285
+ {
3286
+ foreach my $entry (@{$piece})
3287
+ {
3288
+ $groupEntry->GroupContent()->[$index] = $entry;
3289
+ $index++;
3290
+ };
3291
+ };
3292
+ };
3293
+ };
3294
+
3295
+
3296
+ # Okay, the orginal entries are sorted now. Sort the new entries and apply.
3297
+
3298
+ if (scalar @newEntries)
3299
+ {
3300
+ @newEntries = sort { $self->CompareEntries($a, $b) } @newEntries;
3301
+ my @originalEntries = @$groupContent;
3302
+ @$groupContent = ( );
3303
+
3304
+ while (1)
3305
+ {
3306
+ while (scalar @originalEntries && !$self->IsIncludedInSort($groupEntry, $originalEntries[0]))
3307
+ { push @$groupContent, (shift @originalEntries); };
3308
+
3309
+ if (!scalar @originalEntries || !scalar @newEntries)
3310
+ { last; };
3311
+
3312
+ while (scalar @newEntries && $self->CompareEntries($newEntries[0], $originalEntries[0]) < 0)
3313
+ { push @$groupContent, (shift @newEntries); };
3314
+
3315
+ push @$groupContent, (shift @originalEntries);
3316
+
3317
+ if (!scalar @originalEntries || !scalar @newEntries)
3318
+ { last; };
3319
+ };
3320
+
3321
+ if (scalar @originalEntries)
3322
+ { push @$groupContent, @originalEntries; }
3323
+ elsif (scalar @newEntries)
3324
+ { push @$groupContent, @newEntries; };
3325
+ };
3326
+ };
3327
+
3328
+
3329
+ # Now re-add the exceptions.
3330
+
3331
+ if (defined $leadingGeneralIndex)
3332
+ {
3333
+ unshift @{$groupEntry->GroupContent()}, $leadingGeneralIndex;
3334
+ };
3335
+
3336
+ if (defined $trailingIndexGroup)
3337
+ {
3338
+ $groupEntry->PushToGroup($trailingIndexGroup);
3339
+ };
3340
+
3341
+ };
3342
+
3343
+ foreach my $entry (@{$groupEntry->GroupContent()})
3344
+ {
3345
+ if ($entry->Type() == ::MENU_GROUP())
3346
+ { push @groupStack, $entry; };
3347
+ };
3348
+ };
3349
+ };
3350
+
3351
+
3352
+ #
3353
+ # Function: CompareEntries
3354
+ #
3355
+ # A comparison function for use in sorting. Compares the two entries by their titles with <StringCompare()>, but in the case
3356
+ # of a tie, puts <MENU_FILE> entries above <MENU_GROUP> entries.
3357
+ #
3358
+ sub CompareEntries #(a, b)
3359
+ {
3360
+ my ($self, $a, $b) = @_;
3361
+
3362
+ my $result = ::StringCompare($a->Title(), $b->Title());
3363
+
3364
+ if ($result == 0)
3365
+ {
3366
+ if ($a->Type() == ::MENU_FILE() && $b->Type() == ::MENU_GROUP())
3367
+ { $result = -1; }
3368
+ elsif ($a->Type() == ::MENU_GROUP() && $b->Type() == ::MENU_FILE())
3369
+ { $result = 1; };
3370
+ };
3371
+
3372
+ return $result;
3373
+ };
3374
+
3375
+
3376
+ #
3377
+ # Function: SharedDirectoriesOf
3378
+ #
3379
+ # Returns an array of all the directories shared by the files in the group. If none, returns an empty array.
3380
+ #
3381
+ sub SharedDirectoriesOf #(group)
3382
+ {
3383
+ my ($self, $groupEntry) = @_;
3384
+ my @sharedDirectories;
3385
+
3386
+ foreach my $entry (@{$groupEntry->GroupContent()})
3387
+ {
3388
+ if ($entry->Type() == ::MENU_FILE())
3389
+ {
3390
+ my @entryDirectories = NaturalDocs::File->SplitDirectories( (NaturalDocs::File->SplitPath($entry->Target()))[1] );
3391
+
3392
+ if (!scalar @sharedDirectories)
3393
+ { @sharedDirectories = @entryDirectories; }
3394
+ else
3395
+ { ::ShortenToMatchStrings(\@sharedDirectories, \@entryDirectories); };
3396
+
3397
+ if (!scalar @sharedDirectories)
3398
+ { last; };
3399
+ };
3400
+ };
3401
+
3402
+ return @sharedDirectories;
3403
+ };
3404
+
3405
+
3406
+ 1;