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