bixbite 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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;