doc/commands: parse `commands_main`
[nit.git] / src / doc / commands / commands_parser.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # A parser that create DocCommand from a string
16 #
17 # Used by both Nitx and the Markdown doc commands.
18 module commands_parser
19
20 import commands::commands_model
21 import commands::commands_graph
22 import commands::commands_usage
23 import commands::commands_catalog
24 import commands::commands_ini
25 import commands::commands_main
26
27 # Parse string commands to create DocQueries
28 class CommandParser
29
30 # ModelView used to retrieve mentities
31 var view: ModelView
32
33 # ModelBuilder used to retrieve AST nodes
34 var modelbuilder: ModelBuilder
35
36 # Catalog used for catalog commands
37 var catalog: nullable Catalog
38
39 # List of allowed command names for this parser
40 var allowed_commands: Array[String] = [
41 "link", "doc", "code", "lin", "uml", "graph", "search",
42 "parents", "ancestors", "children", "descendants",
43 "param", "return", "new", "call", "defs", "list", "random",
44 "ini-desc", "ini-git", "ini-issues", "ini-maintainer", "ini-contributors", "ini-license",
45 "license-file", "contrib-file", "license-content", "contrib-content", "git-clone",
46 "mains", "main-compile", "main-run", "main-opts", "testing",
47 "catalog", "stats", "tags", "tag", "person", "contrib", "maintain"] is writable
48
49 # List of commands usage and documentation
50 var commands_usage: Map[String, String] do
51 var usage = new ArrayMap[String, String]
52 usage["search: <string>"] = "list entities matching `string`"
53 usage["link: <name>"] = "display the link to `name`"
54 usage["doc: <name>"] = "display the documentation for `name`"
55 usage["defs: <name>"] = "list all definitions for `name`"
56 usage["code: <name>"] = "display the code for `name`"
57 usage["lin: <name>"] = "display the linearization for `name`"
58 usage["uml: <name>"] = "display the UML diagram for `name`"
59 usage["graph: <name>"] = "display the inheritance graph for `name`"
60 usage["parents: <name>"] = "list the direct parents of `name`"
61 usage["ancestors: <name>"] = "list all ancestors of `name`"
62 usage["children: <name>"] = "list direct children of `name`"
63 usage["descendants: <name>"] = "list all descendants of `name`"
64 usage["param: <type>"] = "list all methods accepting `type` as parameter"
65 usage["return: <type>"] = "list all methods returning `type`"
66 usage["new: <class>"] = "list all methods initializing `class`"
67 usage["call: <property>"] = "list all methods calling `property`"
68 usage["list: <kind>"] = "list all entities of `kind` from the model"
69 usage["random: <kind>"] = "list random entities of `kind` from the model"
70 usage["catalog:"] = "list packages from catalog"
71 usage["stats:"] = "display catalog statistics"
72 usage["tags:"] = "list all tabs from catalog"
73 usage["tag: <tag>"] = "list all packages with `tag`"
74 usage["maintain: <person>"] = "list all packages maintained by `person`"
75 usage["contrib: <person>"] = "list all packages contributed by `person`"
76 # Ini commands
77 usage["ini-desc: <package>"] = "display the description from the `package` ini file"
78 usage["ini-git: <package>"] = "display the git url from the `package` ini file"
79 usage["ini-issues: <package>"] = "display the issues url from the `package` ini file"
80 usage["ini-license: <package>"] = "display the license from the `package` ini file"
81 usage["ini-maintainer: <package>"] = "display the maintainer from the `package` ini file"
82 usage["ini-contributors: <package>"] = "display the contributors from the `package` ini file"
83 usage["license-file: <package>"] = "display the license file for the `package`"
84 usage["license-content: <package>"] = "display the license file content for the `package`"
85 usage["contrib-file: <package>"] = "display the contrib file for the `package`"
86 usage["contrib-content: <package>"] = "display the contrib file content for the `package`"
87 usage["git-clone: <package>"] = "display the git clone command for the `package`"
88 # Main
89 usage["mains: <name>"] = "display the list of main methods for `name`"
90 usage["main-compile: <name>"] = "display the nitc command to compile `name`"
91 usage["main-run: <name>"] = "display the command to run `name`"
92 usage["main-opts: <name>"] = "display the command options for `name`"
93 usage["testing: <name>"] = "display the nitunit command to test `name`"
94 return usage
95 end
96
97 # Parse `string` as a DocCommand
98 #
99 # Returns `null` if the string cannot be parsed.
100 # See `error` for the error messages produced by both the parser and the commands.
101 fun parse(string: String): nullable DocCommand do
102 var pos = 0
103 var tmp = new FlatBuffer
104 error = null
105
106 # Parse command name
107 pos = string.read_until(tmp, pos, ':', '|')
108 var name = tmp.write_to_string.trim
109
110 # Check allowed commands
111 if name.is_empty then
112 error = new CmdParserError("Empty command name", 0)
113 return null
114 end
115 # If the command name contains two consecutive colons or there is no colon in the name,
116 # we certainly have a wiki link to a mentity
117 var is_short_link = false
118 if (pos < string.length - 2 and string[pos] == ':' and string[pos + 1] == ':') or
119 pos == string.length then
120 is_short_link = true
121 else if pos < string.length - 1 and string[pos] == '|' then
122 is_short_link = true
123 pos -= 1
124 else if not allowed_commands.has(name) then
125 error = new CmdParserError("Unknown command name `{name}`", 0)
126 return null
127 end
128
129 # Parse the argument
130 tmp.clear
131 pos = string.read_until(tmp, pos + 1, '|')
132 var arg = tmp.write_to_string.trim
133 if is_short_link and not arg.is_empty then
134 arg = "{name}:{arg}"
135 else if is_short_link then
136 arg = name
137 end
138
139 # Parse command options
140 var opts = new HashMap[String, String]
141 while pos < string.length do
142 # Parse option name
143 tmp.clear
144 pos = string.read_until(tmp, pos + 1, ':', ',')
145 var oname = tmp.write_to_string.trim
146 var oval = ""
147 if oname.is_empty then break
148 # Parse option value
149 if pos < string.length and string[pos] == ':' then
150 tmp.clear
151 pos = string.read_until(tmp, pos + 1, ',')
152 oval = tmp.write_to_string.trim
153 end
154 opts[oname] = oval
155 end
156
157 # Build the command
158 var command
159 if is_short_link then
160 command = new CmdEntityLink(view)
161 else
162 command = new_command(name)
163 end
164 if command == null then
165 error = new CmdParserError("Unknown command name `{name}`", 0)
166 return null
167 end
168
169 # Initialize command from string options
170 var status = command.parser_init(arg, opts)
171 if not status isa CmdSuccess then error = status
172
173 return command
174 end
175
176 # Init a new DocCommand from its `name`
177 #
178 # You must redefine this method to add new custom commands.
179 fun new_command(name: String): nullable DocCommand do
180 # CmdEntity
181 if name == "link" then return new CmdEntityLink(view)
182 if name == "doc" then return new CmdComment(view)
183 if name == "code" then return new CmdEntityCode(view, modelbuilder)
184 if name == "lin" then return new CmdLinearization(view)
185 if name == "defs" then return new CmdFeatures(view)
186 if name == "parents" then return new CmdParents(view)
187 if name == "ancestors" then return new CmdAncestors(view)
188 if name == "children" then return new CmdChildren(view)
189 if name == "descendants" then return new CmdDescendants(view)
190 if name == "param" then return new CmdParam(view)
191 if name == "return" then return new CmdReturn(view)
192 if name == "new" then return new CmdNew(view, modelbuilder)
193 if name == "call" then return new CmdCall(view, modelbuilder)
194 # CmdGraph
195 if name == "uml" then return new CmdUML(view)
196 if name == "graph" then return new CmdInheritanceGraph(view)
197 # CmdModel
198 if name == "list" then return new CmdModelEntities(view)
199 if name == "random" then return new CmdRandomEntities(view)
200 # Ini
201 if name == "ini-desc" then return new CmdIniDescription(view)
202 if name == "ini-git" then return new CmdIniGitUrl(view)
203 if name == "ini-issues" then return new CmdIniIssuesUrl(view)
204 if name == "ini-license" then return new CmdIniLicense(view)
205 if name == "ini-maintainer" then return new CmdIniMaintainer(view)
206 if name == "ini-contributors" then return new CmdIniContributors(view)
207 if name == "license-file" then return new CmdLicenseFile(view)
208 if name == "license-content" then return new CmdLicenseFileContent(view)
209 if name == "contrib-file" then return new CmdContribFile(view)
210 if name == "contrib-content" then return new CmdContribFileContent(view)
211 if name == "git-clone" then return new CmdIniCloneCommand(view)
212 # CmdMain
213 if name == "mains" then return new CmdMains(view)
214 if name == "main-compile" then return new CmdMainCompile(view)
215 if name == "main-run" then return new CmdManSynopsis(view)
216 if name == "main-opts" then return new CmdManOptions(view)
217 if name == "testing" then return new CmdTesting(view)
218 # CmdCatalog
219 var catalog = self.catalog
220 if catalog != null then
221 if name == "catalog" then return new CmdCatalogPackages(view, catalog)
222 if name == "stats" then return new CmdCatalogStats(view, catalog)
223 if name == "tags" then return new CmdCatalogTags(view, catalog)
224 if name == "tag" then return new CmdCatalogTag(view, catalog)
225 if name == "person" then return new CmdCatalogPerson(view, catalog)
226 if name == "contrib" then return new CmdCatalogContributing(view, catalog)
227 if name == "maintain" then return new CmdCatalogMaintaining(view, catalog)
228 if name == "search" then return new CmdCatalogSearch(view, catalog)
229 else
230 if name == "search" then return new CmdSearch(view)
231 end
232 return null
233 end
234
235 # Error or warning from last call to `parse`
236 var error: nullable CmdMessage = null
237 end
238
239 # An error produced by the CmdParser
240 class CmdParserError
241 super CmdError
242
243 # Error message
244 var message: String
245
246 # Column related to the error
247 var column: nullable Int
248
249 redef fun to_s do return message
250 end
251
252 redef class DocCommand
253
254 # Initialize the command from the CommandParser data
255 fun parser_init(arg: String, options: Map[String, String]): CmdMessage do
256 return init_command
257 end
258 end
259
260 redef class CmdEntity
261 redef fun parser_init(mentity_name, options) do
262 self.mentity_name = mentity_name
263 return super
264 end
265 end
266
267 redef class CmdList
268 redef fun parser_init(mentity_name, options) do
269 if options.has_key("limit") and options["limit"].is_int then limit = options["limit"].to_i
270 return super
271 end
272 end
273
274 # Model commands
275
276 redef class CmdComment
277 redef fun parser_init(mentity_name, options) do
278 full_doc = not options.has_key("only-synopsis")
279 fallback = not options.has_key("no-fallback")
280 if options.has_key("format") then format = options["format"]
281 return super
282 end
283 end
284
285 redef class CmdEntityLink
286 redef fun parser_init(mentity_name, options) do
287 if options.has_key("text") then text = options["text"]
288 if options.has_key("title") then title = options["title"]
289 return super
290 end
291 end
292
293 redef class CmdCode
294 redef fun parser_init(mentity_name, options) do
295 if options.has_key("format") then format = options["format"]
296 return super
297 end
298 end
299
300 redef class CmdSearch
301 redef fun parser_init(mentity_name, options) do
302 query = mentity_name
303 if options.has_key("page") and options["page"].is_int then page = options["page"].to_i
304 return super
305 end
306 end
307
308 redef class CmdAncestors
309 redef fun parser_init(mentity_name, options) do
310 if options.has_key("parents") and options["parents"] == "false" then parents = false
311 return super
312 end
313 end
314
315 redef class CmdDescendants
316 redef fun parser_init(mentity_name, options) do
317 if options.has_key("children") and options["children"] == "false" then children = false
318 return super
319 end
320 end
321
322 redef class CmdModelEntities
323 redef fun parser_init(kind, options) do
324 self.kind = kind
325 return super
326 end
327 end
328
329 redef class CmdGraph
330 redef fun parser_init(mentity_name, options) do
331 if options.has_key("format") then format = options["format"]
332 return super
333 end
334 end
335
336 redef class CmdInheritanceGraph
337 redef fun parser_init(mentity_name, options) do
338 if options.has_key("pdepth") and options["pdepth"].is_int then
339 pdepth = options["pdepth"].to_i
340 end
341 if options.has_key("cdepth") and options["cdepth"].is_int then
342 cdepth = options["cdepth"].to_i
343 end
344 return super
345 end
346 end
347
348 # Catalog commands
349
350 redef class CmdCatalogTag
351 redef fun parser_init(mentity_name, options) do
352 tag = mentity_name
353 return super
354 end
355 end
356
357 redef class CmdCatalogPerson
358 redef fun parser_init(mentity_name, options) do
359 person_name = mentity_name
360 return super
361 end
362 end
363
364 # Utils
365
366 redef class Text
367 # Read `self` as raw text until `nend` and append it to the `out` buffer.
368 private fun read_until(out: FlatBuffer, start: Int, nend: Char...): Int do
369 var pos = start
370 while pos < length do
371 var c = self[pos]
372 var end_reached = false
373 for n in nend do
374 if c == n then
375 end_reached = true
376 break
377 end
378 end
379 if end_reached then break
380 out.add c
381 pos += 1
382 end
383 return pos
384 end
385 end