modelbuilder: add option `--ignore-visibility`
[nit.git] / src / modelbuilder.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2012 Jean Privat <jean@pryen.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Load nit source files and build the associated model
18 #
19 # FIXME better doc
20 #
21 # FIXME split this module into submodules
22 # FIXME add missing error checks
23 module modelbuilder
24
25 import parser
26 import model
27 import poset
28 import opts
29 import toolcontext
30 import phase
31
32 private import more_collections
33
34 ###
35
36 redef class ToolContext
37 # Option --path
38 var opt_path: OptionArray = new OptionArray("Set include path for loaders (may be used more than once)", "-I", "--path")
39
40 # Option --only-metamodel
41 var opt_only_metamodel: OptionBool = new OptionBool("Stop after meta-model processing", "--only-metamodel")
42
43 # Option --only-parse
44 var opt_only_parse: OptionBool = new OptionBool("Only proceed to parse step of loaders", "--only-parse")
45
46 # Option --ignore-visibility
47 var opt_ignore_visibility: OptionBool = new OptionBool("Do not check, and produce errors, on visibility issues.", "--ignore-visibility")
48
49 redef init
50 do
51 super
52 option_context.add_option(opt_path, opt_only_parse, opt_only_metamodel, opt_ignore_visibility)
53 end
54
55 fun modelbuilder: ModelBuilder do return modelbuilder_real.as(not null)
56 private var modelbuilder_real: nullable ModelBuilder = null
57
58 # Run `process_mainmodule` on all phases
59 fun run_global_phases(mmodules: Array[MModule])
60 do
61 assert not mmodules.is_empty
62 var mainmodule
63 if mmodules.length == 1 then
64 mainmodule = mmodules.first
65 else
66 # We need a main module, so we build it by importing all modules
67 mainmodule = new MModule(modelbuilder.model, null, "<main>", new Location(null, 0, 0, 0, 0))
68 mainmodule.set_imported_mmodules(mmodules)
69 end
70 for phase in phases_list do
71 phase.process_mainmodule(mainmodule, mmodules)
72 end
73 end
74 end
75
76 redef class Phase
77 # Specific action to execute on the whole program.
78 # Called by the `ToolContext::run_global_phases`.
79 #
80 # `mainmodule` is the main module of the program.
81 # It could be an implicit module (called "<main>").
82 #
83 # `given_modules` is the list of explicitely requested modules.
84 # from the command-line for instance.
85 #
86 # REQUIRE: `not given_modules.is_empty`
87 # REQUIRE: `(given_modules.length == 1) == (mainmodule == given_modules.first)`
88 #
89 # @toimplement
90 fun process_mainmodule(mainmodule: MModule, given_mmodules: SequenceRead[MModule]) do end
91 end
92
93
94 # A model builder knows how to load nit source files and build the associated model
95 class ModelBuilder
96 # The model where new modules, classes and properties are added
97 var model: Model
98
99 # The toolcontext used to control the interaction with the user (getting options and displaying messages)
100 var toolcontext: ToolContext
101
102 # Run phases on all loaded modules
103 fun run_phases
104 do
105 var mmodules = model.mmodules.to_a
106 model.mmodule_importation_hierarchy.sort(mmodules)
107 var nmodules = new Array[AModule]
108 for mm in mmodules do
109 nmodules.add(mmodule2nmodule[mm])
110 end
111 toolcontext.run_phases(nmodules)
112
113 if toolcontext.opt_only_metamodel.value then
114 self.toolcontext.info("*** ONLY METAMODEL", 1)
115 exit(0)
116 end
117 end
118
119 # Instantiate a modelbuilder for a model and a toolcontext
120 # Important, the options of the toolcontext must be correctly set (parse_option already called)
121 init(model: Model, toolcontext: ToolContext)
122 do
123 self.model = model
124 self.toolcontext = toolcontext
125 assert toolcontext.modelbuilder_real == null
126 toolcontext.modelbuilder_real = self
127
128 # Setup the paths value
129 paths.append(toolcontext.opt_path.value)
130
131 var path_env = "NIT_PATH".environ
132 if not path_env.is_empty then
133 paths.append(path_env.split_with(':'))
134 end
135
136 path_env = "NIT_DIR".environ
137 if not path_env.is_empty then
138 var libname = "{path_env}/lib"
139 if libname.file_exists then paths.add(libname)
140 end
141
142 var libname = "{sys.program_name.dirname}/../lib"
143 if libname.file_exists then paths.add(libname.simplify_path)
144 end
145
146 # Load a bunch of modules.
147 # `modules` can contains filenames or module names.
148 # Imported modules are automatically loaded and modelized.
149 # The result is the corresponding model elements.
150 # Errors and warnings are printed with the toolcontext.
151 #
152 # Note: class and property model element are not analysed.
153 fun parse(modules: Sequence[String]): Array[MModule]
154 do
155 var time0 = get_time
156 # Parse and recursively load
157 self.toolcontext.info("*** PARSE ***", 1)
158 var mmodules = new ArraySet[MModule]
159 for a in modules do
160 var nmodule = self.load_module(a)
161 if nmodule == null then continue # Skip error
162 mmodules.add(nmodule.mmodule.as(not null))
163 end
164 var time1 = get_time
165 self.toolcontext.info("*** END PARSE: {time1-time0} ***", 2)
166
167 self.toolcontext.check_errors
168
169 if toolcontext.opt_only_parse.value then
170 self.toolcontext.info("*** ONLY PARSE...", 1)
171 exit(0)
172 end
173
174 return mmodules.to_a
175 end
176
177 # Return a class named `name` visible by the module `mmodule`.
178 # Visibility in modules is correctly handled.
179 # If no such a class exists, then null is returned.
180 # If more than one class exists, then an error on `anode` is displayed and null is returned.
181 # FIXME: add a way to handle class name conflict
182 fun try_get_mclass_by_name(anode: ANode, mmodule: MModule, name: String): nullable MClass
183 do
184 var classes = model.get_mclasses_by_name(name)
185 if classes == null then
186 return null
187 end
188
189 var res: nullable MClass = null
190 for mclass in classes do
191 if not mmodule.in_importation <= mclass.intro_mmodule then continue
192 if not mmodule.is_visible(mclass.intro_mmodule, mclass.visibility) then continue
193 if res == null then
194 res = mclass
195 else
196 error(anode, "Ambigous class name '{name}'; conflict between {mclass.full_name} and {res.full_name}")
197 return null
198 end
199 end
200 return res
201 end
202
203 # Return a property named `name` on the type `mtype` visible in the module `mmodule`.
204 # Visibility in modules is correctly handled.
205 # Protected properties are returned (it is up to the caller to check and reject protected properties).
206 # If no such a property exists, then null is returned.
207 # If more than one property exists, then an error on `anode` is displayed and null is returned.
208 # FIXME: add a way to handle property name conflict
209 fun try_get_mproperty_by_name2(anode: ANode, mmodule: MModule, mtype: MType, name: String): nullable MProperty
210 do
211 var props = self.model.get_mproperties_by_name(name)
212 if props == null then
213 return null
214 end
215
216 var cache = self.try_get_mproperty_by_name2_cache[mmodule, mtype, name]
217 if cache != null then return cache
218
219 var res: nullable MProperty = null
220 var ress: nullable Array[MProperty] = null
221 for mprop in props do
222 if not mtype.has_mproperty(mmodule, mprop) then continue
223 if not mmodule.is_visible(mprop.intro_mclassdef.mmodule, mprop.visibility) then continue
224 if res == null then
225 res = mprop
226 else
227 var restype = res.intro_mclassdef.bound_mtype
228 var mproptype = mprop.intro_mclassdef.bound_mtype
229 if restype.is_subtype(mmodule, null, mproptype) then
230 # we keep res
231 else if mproptype.is_subtype(mmodule, null, restype) then
232 res = mprop
233 else
234 if ress == null then ress = new Array[MProperty]
235 ress.add(mprop)
236 end
237 end
238 end
239 if ress != null then
240 var restype = res.intro_mclassdef.bound_mtype
241 for mprop in ress do
242 var mproptype = mprop.intro_mclassdef.bound_mtype
243 if not restype.is_subtype(mmodule, null, mproptype) then
244 self.error(anode, "Ambigous property name '{name}' for {mtype}; conflict between {mprop.full_name} and {res.full_name}")
245 return null
246 end
247 end
248 end
249
250 self.try_get_mproperty_by_name2_cache[mmodule, mtype, name] = res
251 return res
252 end
253
254 private var try_get_mproperty_by_name2_cache: HashMap3[MModule, MType, String, nullable MProperty] = new HashMap3[MModule, MType, String, nullable MProperty]
255
256
257 # Alias for try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.mtype, name)
258 fun try_get_mproperty_by_name(anode: ANode, mclassdef: MClassDef, name: String): nullable MProperty
259 do
260 return try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.bound_mtype, name)
261 end
262
263 # The list of directories to search for top level modules
264 # The list is initially set with :
265 # * the toolcontext --path option
266 # * the NIT_PATH environment variable
267 # * some heuristics including the NIT_DIR environment variable and the progname of the process
268 # Path can be added (or removed) by the client
269 var paths: Array[String] = new Array[String]
270
271 # Get a module by its short name; if required, the module is loaded, parsed and its hierarchies computed.
272 # If `mmodule` is set, then the module search starts from it up to the top level (see `paths`);
273 # if `mmodule` is null then the module is searched in the top level only.
274 # If no module exists or there is a name conflict, then an error on `anode` is displayed and null is returned.
275 # FIXME: add a way to handle module name conflict
276 fun get_mmodule_by_name(anode: ANode, mmodule: nullable MModule, name: String): nullable MModule
277 do
278 # First, look in groups of the module
279 if mmodule != null then
280 var mgroup = mmodule.mgroup
281 while mgroup != null do
282 var dirname = mgroup.filepath
283 if dirname == null then break # virtual group
284 if dirname.has_suffix(".nit") then break # singleton project
285
286 # Second, try the directory to find a file
287 var try_file = dirname + "/" + name + ".nit"
288 if try_file.file_exists then
289 var res = self.load_module(try_file.simplify_path)
290 if res == null then return null # Forward error
291 return res.mmodule.as(not null)
292 end
293
294 # Third, try if the requested module is itself a group
295 try_file = dirname + "/" + name + "/" + name + ".nit"
296 if try_file.file_exists then
297 mgroup = get_mgroup(dirname + "/" + name)
298 var res = self.load_module(try_file.simplify_path)
299 if res == null then return null # Forward error
300 return res.mmodule.as(not null)
301 end
302
303 mgroup = mgroup.parent
304 end
305 end
306
307 # Look at some known directories
308 var lookpaths = self.paths
309
310 # Look in the directory of module project also (even if not explicitely in the path)
311 if mmodule != null and mmodule.mgroup != null then
312 # path of the root group
313 var dirname = mmodule.mgroup.mproject.root.filepath
314 if dirname != null then
315 dirname = dirname.join_path("..").simplify_path
316 if not lookpaths.has(dirname) and dirname.file_exists then
317 lookpaths = lookpaths.to_a
318 lookpaths.add(dirname)
319 end
320 end
321 end
322
323 var candidate = search_module_in_paths(anode.hot_location, name, lookpaths)
324
325 if candidate == null then
326 if mmodule != null then
327 error(anode, "Error: cannot find module {name} from {mmodule}. tried {lookpaths.join(", ")}")
328 else
329 error(anode, "Error: cannot find module {name}. tried {lookpaths.join(", ")}")
330 end
331 return null
332 end
333 var res = self.load_module(candidate)
334 if res == null then return null # Forward error
335 return res.mmodule.as(not null)
336 end
337
338 # Search a module `name` from path `lookpaths`.
339 # If found, the path of the file is returned
340 private fun search_module_in_paths(location: nullable Location, name: String, lookpaths: Collection[String]): nullable String
341 do
342 var candidate: nullable String = null
343 for dirname in lookpaths do
344 var try_file = (dirname + "/" + name + ".nit").simplify_path
345 if try_file.file_exists then
346 if candidate == null then
347 candidate = try_file
348 else if candidate != try_file then
349 # try to disambiguate conflicting modules
350 var abs_candidate = module_absolute_path(candidate)
351 var abs_try_file = module_absolute_path(try_file)
352 if abs_candidate != abs_try_file then
353 toolcontext.error(location, "Error: conflicting module file for {name}: {candidate} {try_file}")
354 end
355 end
356 end
357 try_file = (dirname + "/" + name + "/" + name + ".nit").simplify_path
358 if try_file.file_exists then
359 if candidate == null then
360 candidate = try_file
361 else if candidate != try_file then
362 # try to disambiguate conflicting modules
363 var abs_candidate = module_absolute_path(candidate)
364 var abs_try_file = module_absolute_path(try_file)
365 if abs_candidate != abs_try_file then
366 toolcontext.error(location, "Error: conflicting module file for {name}: {candidate} {try_file}")
367 end
368 end
369 end
370 end
371 return candidate
372 end
373
374 # cache for `identify_file` by realpath
375 private var identified_files = new HashMap[String, nullable ModulePath]
376
377 # Identify a source file
378 # Load the associated project and groups if required
379 private fun identify_file(path: String): nullable ModulePath
380 do
381 # special case for not a nit file
382 if path.file_extension != "nit" then
383 # search in known -I paths
384 var candidate = search_module_in_paths(null, path, self.paths)
385
386 # Found nothins? maybe it is a group...
387 if candidate == null and path.file_exists then
388 var mgroup = get_mgroup(path)
389 if mgroup != null then
390 var owner_path = mgroup.filepath.join_path(mgroup.name + ".nit")
391 if owner_path.file_exists then candidate = owner_path
392 end
393 end
394
395 if candidate == null then
396 toolcontext.error(null, "Error: cannot find module `{path}`.")
397 return null
398 end
399 path = candidate
400 end
401
402 # Fast track, the path is already known
403 var pn = path.basename(".nit")
404 var rp = module_absolute_path(path)
405 if identified_files.has_key(rp) then return identified_files[rp]
406
407 # Search for a group
408 var mgrouppath = path.join_path("..").simplify_path
409 var mgroup = get_mgroup(mgrouppath)
410
411 if mgroup == null then
412 # singleton project
413 var mproject = new MProject(pn, model)
414 mgroup = new MGroup(pn, mproject, null) # same name for the root group
415 mgroup.filepath = path
416 mproject.root = mgroup
417 toolcontext.info("found project `{pn}` at {path}", 2)
418 end
419
420 var res = new ModulePath(pn, path, mgroup)
421 mgroup.module_paths.add(res)
422
423 identified_files[rp] = res
424 return res
425 end
426
427 # groups by path
428 private var mgroups = new HashMap[String, nullable MGroup]
429
430 # return the mgroup associated to a directory path
431 # if the directory is not a group null is returned
432 private fun get_mgroup(dirpath: String): nullable MGroup
433 do
434 var rdp = module_absolute_path(dirpath)
435 if mgroups.has_key(rdp) then
436 return mgroups[rdp]
437 end
438
439 # Hack, a dir is determined by the presence of a honomymous nit file
440 var pn = rdp.basename(".nit")
441 var mp = dirpath.join_path(pn + ".nit").simplify_path
442
443 if not mp.file_exists then return null
444
445 # check parent directory
446 var parentpath = dirpath.join_path("..").simplify_path
447 var parent = get_mgroup(parentpath)
448
449 var mgroup
450 if parent == null then
451 # no parent, thus new project
452 var mproject = new MProject(pn, model)
453 mgroup = new MGroup(pn, mproject, null) # same name for the root group
454 mproject.root = mgroup
455 toolcontext.info("found project `{mproject}` at {dirpath}", 2)
456 else
457 mgroup = new MGroup(pn, parent.mproject, parent)
458 toolcontext.info("found sub group `{mgroup.full_name}` at {dirpath}", 2)
459 end
460 mgroup.filepath = dirpath
461 mgroups[rdp] = mgroup
462 return mgroup
463 end
464
465 # Transform relative paths (starting with '../') into absolute paths
466 private fun module_absolute_path(path: String): String do
467 return getcwd.join_path(path).simplify_path
468 end
469
470 # Try to load a module AST using a path.
471 # Display an error if there is a problem (IO / lexer / parser) and return null
472 fun load_module_ast(filename: String): nullable AModule
473 do
474 if filename.file_extension != "nit" then
475 self.toolcontext.error(null, "Error: file {filename} is not a valid nit module.")
476 return null
477 end
478 if not filename.file_exists then
479 self.toolcontext.error(null, "Error: file {filename} not found.")
480 return null
481 end
482
483 self.toolcontext.info("load module {filename}", 2)
484
485 # Load the file
486 var file = new IFStream.open(filename)
487 var lexer = new Lexer(new SourceFile(filename, file))
488 var parser = new Parser(lexer)
489 var tree = parser.parse
490 file.close
491 var mod_name = filename.basename(".nit")
492
493 # Handle lexer and parser error
494 var nmodule = tree.n_base
495 if nmodule == null then
496 var neof = tree.n_eof
497 assert neof isa AError
498 error(neof, neof.message)
499 return null
500 end
501
502 return nmodule
503 end
504
505 # Try to load a module and its imported modules using a path.
506 # Display an error if there is a problem (IO / lexer / parser / importation) and return null
507 # Note: usually, you do not need this method, use `get_mmodule_by_name` instead.
508 fun load_module(filename: String): nullable AModule
509 do
510 # Look for the module
511 var file = identify_file(filename)
512 if file == null then return null # forward error
513
514 # Already known and loaded? then return it
515 var mmodule = file.mmodule
516 if mmodule != null then
517 return mmodule2nmodule[mmodule]
518 end
519
520 # Load it manually
521 var nmodule = load_module_ast(file.filepath)
522 if nmodule == null then return null # forward error
523
524 # build the mmodule and load imported modules
525 mmodule = build_a_mmodule(file.mgroup, file.name, nmodule)
526
527 if mmodule == null then return null # forward error
528
529 # Update the file information
530 file.mmodule = mmodule
531
532 # Load imported module
533 build_module_importation(nmodule)
534
535 return nmodule
536 end
537
538 fun load_rt_module(parent: MModule, nmodule: AModule, mod_name: String): nullable AModule
539 do
540 # Create the module
541 var mmodule = new MModule(model, parent.mgroup, mod_name, nmodule.location)
542 nmodule.mmodule = mmodule
543 nmodules.add(nmodule)
544 self.mmodule2nmodule[mmodule] = nmodule
545
546 var imported_modules = new Array[MModule]
547
548 imported_modules.add(parent)
549 mmodule.set_visibility_for(parent, intrude_visibility)
550
551 mmodule.set_imported_mmodules(imported_modules)
552
553 return nmodule
554 end
555
556 # Visit the AST and create the `MModule` object
557 private fun build_a_mmodule(mgroup: nullable MGroup, mod_name: String, nmodule: AModule): nullable MModule
558 do
559 # Check the module name
560 var decl = nmodule.n_moduledecl
561 if decl == null then
562 #warning(nmodule, "Warning: Missing 'module' keyword") #FIXME: NOT YET FOR COMPATIBILITY
563 else
564 var decl_name = decl.n_name.n_id.text
565 if decl_name != mod_name then
566 error(decl.n_name, "Error: module name missmatch; declared {decl_name} file named {mod_name}")
567 end
568 end
569
570 # Create the module
571 var mmodule = new MModule(model, mgroup, mod_name, nmodule.location)
572 nmodule.mmodule = mmodule
573 nmodules.add(nmodule)
574 self.mmodule2nmodule[mmodule] = nmodule
575
576 if decl != null then
577 var ndoc = decl.n_doc
578 if ndoc != null then mmodule.mdoc = ndoc.to_mdoc
579 end
580
581 return mmodule
582 end
583
584 # Analysis the module importation and fill the module_importation_hierarchy
585 private fun build_module_importation(nmodule: AModule)
586 do
587 if nmodule.is_importation_done then return
588 nmodule.is_importation_done = true
589 var mmodule = nmodule.mmodule.as(not null)
590 var stdimport = true
591 var imported_modules = new Array[MModule]
592 for aimport in nmodule.n_imports do
593 stdimport = false
594 if not aimport isa AStdImport then
595 continue
596 end
597 var mod_name = aimport.n_name.n_id.text
598 var sup = self.get_mmodule_by_name(aimport.n_name, mmodule, mod_name)
599 if sup == null then continue # Skip error
600 aimport.mmodule = sup
601 imported_modules.add(sup)
602 var mvisibility = aimport.n_visibility.mvisibility
603 if mvisibility == protected_visibility then
604 error(aimport.n_visibility, "Error: only properties can be protected.")
605 return
606 end
607 if sup == mmodule then
608 error(aimport.n_name, "Error: Dependency loop in module {mmodule}.")
609 end
610 if sup.in_importation < mmodule then
611 error(aimport.n_name, "Error: Dependency loop between modules {mmodule} and {sup}.")
612 return
613 end
614 mmodule.set_visibility_for(sup, mvisibility)
615 end
616 if stdimport then
617 var mod_name = "standard"
618 var sup = self.get_mmodule_by_name(nmodule, null, mod_name)
619 if sup != null then # Skip error
620 imported_modules.add(sup)
621 mmodule.set_visibility_for(sup, public_visibility)
622 end
623 end
624 self.toolcontext.info("{mmodule} imports {imported_modules.join(", ")}", 3)
625 mmodule.set_imported_mmodules(imported_modules)
626 end
627
628 # All the loaded modules
629 var nmodules: Array[AModule] = new Array[AModule]
630
631 # Register the nmodule associated to each mmodule
632 # FIXME: why not refine the `MModule` class with a nullable attribute?
633 var mmodule2nmodule: HashMap[MModule, AModule] = new HashMap[MModule, AModule]
634
635 # Helper function to display an error on a node.
636 # Alias for `self.toolcontext.error(n.hot_location, text)`
637 fun error(n: ANode, text: String)
638 do
639 self.toolcontext.error(n.hot_location, text)
640 end
641
642 # Helper function to display a warning on a node.
643 # Alias for: `self.toolcontext.warning(n.hot_location, text)`
644 fun warning(n: ANode, text: String)
645 do
646 self.toolcontext.warning(n.hot_location, text)
647 end
648
649 # Force to get the primitive method named `name` on the type `recv` or do a fatal error on `n`
650 fun force_get_primitive_method(n: ANode, name: String, recv: MClass, mmodule: MModule): MMethod
651 do
652 var res = mmodule.try_get_primitive_method(name, recv)
653 if res == null then
654 self.toolcontext.fatal_error(n.hot_location, "Fatal Error: {recv} must have a property named {name}.")
655 abort
656 end
657 return res
658 end
659 end
660
661 # placeholder to a module file identified but not always loaded in a project
662 private class ModulePath
663 # The name of the module
664 # (it's the basename of the filepath)
665 var name: String
666
667 # The human path of the module
668 var filepath: String
669
670 # The group (and the project) of the possible module
671 var mgroup: MGroup
672
673 # The loaded module (if any)
674 var mmodule: nullable MModule = null
675
676 redef fun to_s do return filepath
677 end
678
679 redef class MGroup
680 # modules paths associated with the group
681 private var module_paths = new Array[ModulePath]
682 end
683
684 redef class AStdImport
685 # The imported module once determined
686 var mmodule: nullable MModule = null
687 end
688
689 redef class AModule
690 # The associated MModule once build by a `ModelBuilder`
691 var mmodule: nullable MModule
692 # Flag that indicate if the importation is already completed
693 var is_importation_done: Bool = false
694 end
695
696 redef class AVisibility
697 # The visibility level associated with the AST node class
698 fun mvisibility: MVisibility is abstract
699 end
700 redef class AIntrudeVisibility
701 redef fun mvisibility do return intrude_visibility
702 end
703 redef class APublicVisibility
704 redef fun mvisibility do return public_visibility
705 end
706 redef class AProtectedVisibility
707 redef fun mvisibility do return protected_visibility
708 end
709 redef class APrivateVisibility
710 redef fun mvisibility do return private_visibility
711 end
712
713 redef class ADoc
714 private var mdoc_cache: nullable MDoc
715 fun to_mdoc: MDoc
716 do
717 var res = mdoc_cache
718 if res != null then return res
719 res = new MDoc
720 for c in n_comment do
721 var text = c.text
722 if text.length < 2 then
723 res.content.add ""
724 continue
725 end
726 assert text.chars[0] == '#'
727 if text.chars[1] == ' ' then
728 text = text.substring_from(2) # eat starting `#` and space
729 else
730 text = text.substring_from(1) # eat atarting `#` only
731 end
732 if text.chars.last == '\n' then text = text.substring(0, text.length-1) # drop \n
733 res.content.add(text)
734 end
735 mdoc_cache = res
736 return res
737 end
738 end