phase: add option --disable-phase
[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 redef init
47 do
48 super
49 option_context.add_option(opt_path, opt_only_parse, opt_only_metamodel)
50 end
51
52 fun modelbuilder: ModelBuilder do return modelbuilder_real.as(not null)
53 private var modelbuilder_real: nullable ModelBuilder = null
54
55 # Run `process_mainmodule` on all phases
56 fun run_global_phases(mmodules: Array[MModule])
57 do
58 assert not mmodules.is_empty
59 var mainmodule
60 if mmodules.length == 1 then
61 mainmodule = mmodules.first
62 else
63 # We need a main module, so we build it by importing all modules
64 mainmodule = new MModule(modelbuilder.model, null, "<main>", new Location(null, 0, 0, 0, 0))
65 mainmodule.set_imported_mmodules(mmodules)
66 end
67 for phase in phases_list do
68 if phase.disabled then continue
69 phase.process_mainmodule(mainmodule, mmodules)
70 end
71 end
72 end
73
74 redef class Phase
75 # Specific action to execute on the whole program.
76 # Called by the `ToolContext::run_global_phases`.
77 #
78 # `mainmodule` is the main module of the program.
79 # It could be an implicit module (called "<main>").
80 #
81 # `given_modules` is the list of explicitely requested modules.
82 # from the command-line for instance.
83 #
84 # REQUIRE: `not given_modules.is_empty`
85 # REQUIRE: `(given_modules.length == 1) == (mainmodule == given_modules.first)`
86 #
87 # @toimplement
88 fun process_mainmodule(mainmodule: MModule, given_mmodules: SequenceRead[MModule]) do end
89 end
90
91
92 # A model builder knows how to load nit source files and build the associated model
93 class ModelBuilder
94 # The model where new modules, classes and properties are added
95 var model: Model
96
97 # The toolcontext used to control the interaction with the user (getting options and displaying messages)
98 var toolcontext: ToolContext
99
100 # Run phases on all loaded modules
101 fun run_phases
102 do
103 var mmodules = model.mmodules.to_a
104 model.mmodule_importation_hierarchy.sort(mmodules)
105 var nmodules = new Array[AModule]
106 for mm in mmodules do
107 nmodules.add(mmodule2nmodule[mm])
108 end
109 toolcontext.run_phases(nmodules)
110
111 if toolcontext.opt_only_metamodel.value then
112 self.toolcontext.info("*** ONLY METAMODEL", 1)
113 exit(0)
114 end
115 end
116
117 # Instantiate a modelbuilder for a model and a toolcontext
118 # Important, the options of the toolcontext must be correctly set (parse_option already called)
119 init(model: Model, toolcontext: ToolContext)
120 do
121 self.model = model
122 self.toolcontext = toolcontext
123 assert toolcontext.modelbuilder_real == null
124 toolcontext.modelbuilder_real = self
125
126 # Setup the paths value
127 paths.append(toolcontext.opt_path.value)
128
129 var path_env = "NIT_PATH".environ
130 if not path_env.is_empty then
131 paths.append(path_env.split_with(':'))
132 end
133
134 path_env = "NIT_DIR".environ
135 if not path_env.is_empty then
136 var libname = "{path_env}/lib"
137 if libname.file_exists then paths.add(libname)
138 end
139
140 var libname = "{sys.program_name.dirname}/../lib"
141 if libname.file_exists then paths.add(libname.simplify_path)
142 end
143
144 # Load a bunch of modules.
145 # `modules` can contains filenames or module names.
146 # Imported modules are automatically loaded and modelized.
147 # The result is the corresponding model elements.
148 # Errors and warnings are printed with the toolcontext.
149 #
150 # Note: class and property model element are not analysed.
151 fun parse(modules: Sequence[String]): Array[MModule]
152 do
153 var time0 = get_time
154 # Parse and recursively load
155 self.toolcontext.info("*** PARSE ***", 1)
156 var mmodules = new ArraySet[MModule]
157 for a in modules do
158 var nmodule = self.load_module(a)
159 if nmodule == null then continue # Skip error
160 mmodules.add(nmodule.mmodule.as(not null))
161 end
162 var time1 = get_time
163 self.toolcontext.info("*** END PARSE: {time1-time0} ***", 2)
164
165 self.toolcontext.check_errors
166
167 if toolcontext.opt_only_parse.value then
168 self.toolcontext.info("*** ONLY PARSE...", 1)
169 exit(0)
170 end
171
172 return mmodules.to_a
173 end
174
175 # Return a class named `name` visible by the module `mmodule`.
176 # Visibility in modules is correctly handled.
177 # If no such a class exists, then null is returned.
178 # If more than one class exists, then an error on `anode` is displayed and null is returned.
179 # FIXME: add a way to handle class name conflict
180 fun try_get_mclass_by_name(anode: ANode, mmodule: MModule, name: String): nullable MClass
181 do
182 var classes = model.get_mclasses_by_name(name)
183 if classes == null then
184 return null
185 end
186
187 var res: nullable MClass = null
188 for mclass in classes do
189 if not mmodule.in_importation <= mclass.intro_mmodule then continue
190 if not mmodule.is_visible(mclass.intro_mmodule, mclass.visibility) then continue
191 if res == null then
192 res = mclass
193 else
194 error(anode, "Ambigous class name '{name}'; conflict between {mclass.full_name} and {res.full_name}")
195 return null
196 end
197 end
198 return res
199 end
200
201 # Return a property named `name` on the type `mtype` visible in the module `mmodule`.
202 # Visibility in modules is correctly handled.
203 # Protected properties are returned (it is up to the caller to check and reject protected properties).
204 # If no such a property exists, then null is returned.
205 # If more than one property exists, then an error on `anode` is displayed and null is returned.
206 # FIXME: add a way to handle property name conflict
207 fun try_get_mproperty_by_name2(anode: ANode, mmodule: MModule, mtype: MType, name: String): nullable MProperty
208 do
209 var props = self.model.get_mproperties_by_name(name)
210 if props == null then
211 return null
212 end
213
214 var cache = self.try_get_mproperty_by_name2_cache[mmodule, mtype, name]
215 if cache != null then return cache
216
217 var res: nullable MProperty = null
218 var ress: nullable Array[MProperty] = null
219 for mprop in props do
220 if not mtype.has_mproperty(mmodule, mprop) then continue
221 if not mmodule.is_visible(mprop.intro_mclassdef.mmodule, mprop.visibility) then continue
222 if res == null then
223 res = mprop
224 else
225 var restype = res.intro_mclassdef.bound_mtype
226 var mproptype = mprop.intro_mclassdef.bound_mtype
227 if restype.is_subtype(mmodule, null, mproptype) then
228 # we keep res
229 else if mproptype.is_subtype(mmodule, null, restype) then
230 res = mprop
231 else
232 if ress == null then ress = new Array[MProperty]
233 ress.add(mprop)
234 end
235 end
236 end
237 if ress != null then
238 var restype = res.intro_mclassdef.bound_mtype
239 for mprop in ress do
240 var mproptype = mprop.intro_mclassdef.bound_mtype
241 if not restype.is_subtype(mmodule, null, mproptype) then
242 self.error(anode, "Ambigous property name '{name}' for {mtype}; conflict between {mprop.full_name} and {res.full_name}")
243 return null
244 end
245 end
246 end
247
248 self.try_get_mproperty_by_name2_cache[mmodule, mtype, name] = res
249 return res
250 end
251
252 private var try_get_mproperty_by_name2_cache: HashMap3[MModule, MType, String, nullable MProperty] = new HashMap3[MModule, MType, String, nullable MProperty]
253
254
255 # Alias for try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.mtype, name)
256 fun try_get_mproperty_by_name(anode: ANode, mclassdef: MClassDef, name: String): nullable MProperty
257 do
258 return try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.bound_mtype, name)
259 end
260
261 # The list of directories to search for top level modules
262 # The list is initially set with :
263 # * the toolcontext --path option
264 # * the NIT_PATH environment variable
265 # * some heuristics including the NIT_DIR environment variable and the progname of the process
266 # Path can be added (or removed) by the client
267 var paths: Array[String] = new Array[String]
268
269 # Get a module by its short name; if required, the module is loaded, parsed and its hierarchies computed.
270 # If `mmodule` is set, then the module search starts from it up to the top level (see `paths`);
271 # if `mmodule` is null then the module is searched in the top level only.
272 # If no module exists or there is a name conflict, then an error on `anode` is displayed and null is returned.
273 # FIXME: add a way to handle module name conflict
274 fun get_mmodule_by_name(anode: ANode, mmodule: nullable MModule, name: String): nullable MModule
275 do
276 # First, look in groups of the module
277 if mmodule != null then
278 var mgroup = mmodule.mgroup
279 while mgroup != null do
280 var dirname = mgroup.filepath
281 if dirname == null then break # virtual group
282 if dirname.has_suffix(".nit") then break # singleton project
283
284 # Second, try the directory to find a file
285 var try_file = dirname + "/" + name + ".nit"
286 if try_file.file_exists then
287 var res = self.load_module(try_file.simplify_path)
288 if res == null then return null # Forward error
289 return res.mmodule.as(not null)
290 end
291
292 # Third, try if the requested module is itself a group
293 try_file = dirname + "/" + name + "/" + name + ".nit"
294 if try_file.file_exists then
295 mgroup = get_mgroup(dirname + "/" + name)
296 var res = self.load_module(try_file.simplify_path)
297 if res == null then return null # Forward error
298 return res.mmodule.as(not null)
299 end
300
301 mgroup = mgroup.parent
302 end
303 end
304
305 # Look at some known directories
306 var lookpaths = self.paths
307
308 # Look in the directory of module project also (even if not explicitely in the path)
309 if mmodule != null and mmodule.mgroup != null then
310 # path of the root group
311 var dirname = mmodule.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 mmodule != null then
325 error(anode, "Error: cannot find module {name} from {mmodule}. 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, 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