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