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