nitiwiki: protect arguments of sys.system
[nit.git] / contrib / nitiwiki / src / wiki_base.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Base entities of a nitiwiki.
16 module wiki_base
17
18 import template::macro
19 import opts
20 import ini
21
22 # A Nitiwiki instance.
23 #
24 # Nitiwiki provide all base services used by `WikiSection` and `WikiArticle`.
25 # It manages content and renders pages.
26 #
27 # Each nitiwiki instance is linked to a config file.
28 # This file show to `nitiwiki` that a wiki is present in the current directory.
29 # Without it, nitiwiki will consider the directory as empty.
30 class Nitiwiki
31
32 # Wiki config object.
33 var config: WikiConfig
34
35 # Default config filename.
36 var config_filename = "config.ini"
37
38 # Force render on all file even if the source is unmodified.
39 var force_render = false is writable
40
41 # Verbosity level.
42 var verbose_level = 0 is writable
43
44 # Delete all the output files.
45 fun clean do
46 var out_dir = expand_path(config.root_dir, config.out_dir)
47 if out_dir.file_exists then out_dir.rmdir
48 end
49
50 # Synchronize local output with the distant `WikiConfig::rsync_dir`.
51 fun sync do
52 var root = expand_path(config.root_dir, config.out_dir)
53 var rsync_dir = config.rsync_dir
54 sys.system "rsync -vr --delete -- {root.escape_to_sh}/ {rsync_dir.escape_to_sh}"
55 end
56
57 # Pull data from git repository.
58 fun fetch do
59 sys.system "git pull {config.git_origin.escape_to_sh} {config.git_branch.escape_to_sh}"
60 end
61
62 # Analyze wiki files from `dir` to build wiki entries.
63 #
64 # This method build a hierarchical structure of `WikiSection` and `WikiArticle`
65 # based on the markdown source structure.
66 fun parse do
67 var dir = expand_path(config.root_dir, config.source_dir)
68 root_section = new_section(dir)
69 var files = list_md_files(dir)
70 for file in files do
71 new_article(file)
72 end
73 end
74
75 # Render output.
76 fun render do end
77
78 # Show wiki status.
79 fun status do
80 print "nitiWiki"
81 print "name: {config.wiki_name}"
82 print "config: {config.ini_file}"
83 print ""
84 if root_section.is_dirty then
85 print "There is modified files:"
86 var paths = entries.keys.to_a
87 var s = new DefaultComparator
88 s.sort(paths)
89 for path in paths do
90 var entry = entries[path]
91 if not entry.is_dirty then continue
92 var name = entry.name
93 if entry.has_source then name = entry.src_path.to_s
94 if entry.is_new then
95 print " + {name}"
96 else
97 print " * {name}"
98 end
99 end
100 print ""
101 print "Use nitiwiki --render to render modified files"
102 else
103 print "Wiki is up-to-date"
104 print ""
105 print "Use nitiwiki --fetch to pull modification from origin"
106 print "Use nitiwiki --rsync to synchronize distant output"
107 end
108 end
109
110 # Display msg if `level <= verbose_level`
111 fun message(msg: String, level: Int) do
112 if level <= verbose_level then print msg
113 end
114
115 # List markdown source files from a directory.
116 fun list_md_files(dir: String): Array[String] do
117 var files = new Array[String]
118 var pipe = new ProcessReader("find", dir, "-name", "*.{config.md_ext}")
119 while not pipe.eof do
120 var file = pipe.read_line
121 if file == "" then break # last line
122 var name = file.basename(".{config.md_ext}")
123 if name == "header" or name == "footer" or name == "menu" then continue
124 files.add file
125 end
126 pipe.close
127 pipe.wait
128 if pipe.status != 0 then exit 1
129 var s = new DefaultComparator
130 s.sort(files)
131 return files
132 end
133
134 # Does `src` have been modified since `target` creation?
135 #
136 # Always returns `true` if `--force` is on.
137 fun need_render(src, target: String): Bool do
138 if force_render then return true
139 if not target.file_exists then return true
140 return src.file_stat.mtime >= target.file_stat.mtime
141 end
142
143 # Create a new `WikiSection`.
144 #
145 # `path` is used to determine the place in the wiki hierarchy.
146 protected fun new_section(path: String): WikiSection do
147 path = path.simplify_path
148 if entries.has_key(path) then return entries[path].as(WikiSection)
149 var root = expand_path(config.root_dir, config.source_dir)
150 var name = path.basename("")
151 var section = new WikiSection(self, name)
152 entries[path] = section
153 if path == root then return section
154 var ppath = path.dirname
155 if ppath != path then
156 var parent = new_section(ppath)
157 parent.add_child(section)
158 end
159 section.try_load_config
160 return section
161 end
162
163 # Create a new `WikiArticle`.
164 #
165 # `path` is used to determine the ancestor sections.
166 protected fun new_article(path: String): WikiArticle do
167 if entries.has_key(path) then return entries[path].as(WikiArticle)
168 message("Found article `{path}`", 2)
169 var article = new WikiArticle.from_source(self, path)
170 var section = new_section(path.dirname)
171 section.add_child(article)
172 entries[path] = article
173 return article
174 end
175
176 # Wiki entries found in the last `lookup_hierarchy`.
177 var entries = new HashMap[String, WikiEntry]
178
179 # The root `WikiSection` of the site found in the last `lookup_hierarchy`.
180 var root_section: WikiSection is noinit
181
182 # Does a template named `name` exists for this wiki?
183 fun has_template(name: String): Bool do
184 return expand_path(config.root_dir, config.templates_dir, name).file_exists
185 end
186
187 # Load a template file as a `TemplateString`.
188 #
189 # REQUIRE: `has_template`
190 fun load_template(name: String): TemplateString do
191 if not has_template(name) then
192 message("Error: can't load template `{name}`", 0)
193 exit 1
194 end
195 var file = expand_path(config.root_dir, config.templates_dir, name)
196 var tpl = new TemplateString.from_file(file)
197 if tpl.has_macro("TITLE") then
198 tpl.replace("TITLE", config.wiki_name)
199 end
200 if tpl.has_macro("SUBTITLE") then
201 tpl.replace("SUBTITLE", config.wiki_desc)
202 end
203 if tpl.has_macro("LOGO") then
204 tpl.replace("LOGO", config.wiki_logo)
205 end
206 return tpl
207 end
208
209 # Does a sideblock named `name` exists for this wiki?
210 fun has_sideblock(name: String): Bool do
211 name = "{name}.{config.md_ext}"
212 return expand_path(config.root_dir, config.sidebar_dir, name).file_exists
213 end
214
215 # Load a markdown block with `name` from `WikiConfig::sidebar_dir`.
216 private fun load_sideblock(name: String): nullable String do
217 if not has_sideblock(name) then
218 message("Error: can't load sideblock `{name}`", 0)
219 return null
220 end
221 name = "{name}.{config.md_ext}"
222 var path = expand_path(config.root_dir, config.sidebar_dir, name)
223 var file = new FileReader.open(path)
224 var res = file.read_all
225 file.close
226 return res
227 end
228
229 # Join `parts` as a path and simplify it
230 fun expand_path(parts: String...): String do
231 var path = ""
232 for part in parts do
233 path = path.join_path(part)
234 end
235 return path.simplify_path
236 end
237
238 # Transform an id style name into a pretty printed name.
239 #
240 # Used to translate ids in beautiful page names.
241 fun pretty_name(name: String): String do
242 name = name.replace("_", " ")
243 name = name.capitalized
244 return name
245 end
246 end
247
248 # A wiki is composed of hierarchical entries.
249 abstract class WikiEntry
250
251 # `Nitiwiki` this entry belongs to.
252 var wiki: Nitiwiki
253
254 # Entry data
255
256 # Entry internal name.
257 #
258 # Mainly used in urls.
259 var name: String
260
261 # Displayed title for `self`.
262 #
263 # If `self` is the root entry then display the wiki `WikiConfig::wiki_name` instead.
264 fun title: String do
265 if is_root then return wiki.config.wiki_name
266 return wiki.pretty_name(name)
267 end
268
269 # Is this section rendered from a source document?
270 #
271 # Source is an abstract concept at this level.
272 # It can represent a directory, a source file,
273 # a part of a file, everything needed to
274 # extend this base framework.
275 fun has_source: Bool do return src_path != null
276
277 # Entry creation time.
278 #
279 # Returns `-1` if not `has_source`.
280 fun create_time: Int do
281 if not has_source then return -1
282 return src_full_path.file_stat.ctime
283 end
284
285 # Entry last modification time.
286 #
287 # Returns `-1` if not `has_source`.
288 fun last_edit_time: Int do
289 if not has_source then return -1
290 return src_full_path.file_stat.mtime
291 end
292
293 # Entry list rendering time.
294 #
295 # Returns `-1` if `is_new`.
296 fun last_render_time: Int do
297 if is_new then return -1
298 return out_full_path.file_stat.mtime
299 end
300
301 # Entries hierarchy
302
303 # Type of the parent entry.
304 type PARENT: WikiEntry
305
306 # Parent entry if any.
307 var parent: nullable PARENT = null
308
309 # Does `self` have a parent?
310 fun is_root: Bool do return parent == null
311
312 # Children labelled by `name`.
313 var children = new HashMap[String, WikiEntry]
314
315 # Does `self` have a child nammed `name`?
316 fun has_child(name: String): Bool do return children.keys.has(name)
317
318 # Retrieve the child called `name`.
319 fun child(name: String): WikiEntry do return children[name]
320
321 # Add a sub-entry to `self`.
322 fun add_child(entry: WikiEntry) do
323 entry.parent = self
324 children[entry.name] = entry
325 end
326
327 # Paths and urls
328
329 # Breadcrumbs from the `Nitiwiki::root_section` to `self`.
330 #
331 # Result is returned as an array containg ordered entries:
332 # `breadcrumbs.first` is the root entry and
333 # `breadcrumbs.last == self`
334 var breadcrumbs: Array[WikiEntry] is lazy do
335 var path = new Array[WikiEntry]
336 var entry: nullable WikiEntry = self
337 while entry != null and not entry.is_root do
338 path.add entry
339 entry = entry.parent
340 end
341 return path.reversed
342 end
343
344 # Sidebar relative to this wiki entry.
345 var sidebar = new WikiSidebar(self)
346
347 # Relative path from `wiki.config.root_dir` to source if any.
348 fun src_path: nullable String is abstract
349
350 # Absolute path to the source if any.
351 fun src_full_path: nullable String do
352 var src = src_path
353 if src == null then return null
354 return wiki.config.root_dir.join_path(src)
355 end
356
357 # Relative path from `wiki.config.root_dir` to rendered output.
358 #
359 # Like `src_path`, this method can represent a
360 # directory or a file.
361 fun out_path: String is abstract
362
363 # Absolute path to the output.
364 fun out_full_path: String do return wiki.config.root_dir.join_path(out_path)
365
366 # Rendering
367
368 # Does `self` have already been rendered?
369 fun is_new: Bool do return not out_full_path.file_exists
370
371 # Does `self` rendered output is outdated?
372 #
373 # Returns `true` if `is_new` then check in children.
374 fun is_dirty: Bool do
375 if is_new then return true
376 if has_source then
377 if last_edit_time >= last_render_time then return true
378 end
379 for child in children.values do
380 if child.is_dirty then return true
381 end
382 return false
383 end
384
385 # Render `self` and `children` is needed.
386 fun render do for child in children.values do child.render
387
388 # Templating
389
390 # Template file for `self`.
391 #
392 # Each entity can use a custom template.
393 # By default the template is inherited from the parent.
394 #
395 # If the root does not have a custom template,
396 # then returns the main wiki template file.
397 fun template_file: String do
398 if is_root then return wiki.config.template_file
399 return parent.template_file
400 end
401
402 # Header template file for `self`.
403 #
404 # Behave like `template_file`.
405 fun header_file: String do
406 if is_root then return wiki.config.header_file
407 return parent.header_file
408 end
409
410 # Footer template file for `self`.
411 #
412 # Behave like `template_file`.
413 fun footer_file: String do
414 if is_root then return wiki.config.footer_file
415 return parent.footer_file
416 end
417
418 # Menu template file for `self`.
419 #
420 # Behave like `template_file`.
421 fun menu_file: String do
422 if is_root then return wiki.config.menu_file
423 return parent.menu_file
424 end
425
426 # Display the entry `name`.
427 redef fun to_s do return name
428 end
429
430 # Each WikiSection is related to a source directory.
431 #
432 # A section can contain other sub-sections or pages.
433 class WikiSection
434 super WikiEntry
435
436 # A section can only have another section as parent.
437 redef type PARENT: WikiSection
438
439 redef fun title do
440 if has_config then
441 var title = config.title
442 if title != null then return title
443 end
444 return super
445 end
446
447 # Is this section hidden?
448 #
449 # Hidden section are rendered but not linked in menus.
450 fun is_hidden: Bool do
451 if has_config then return config.is_hidden
452 return false
453 end
454
455 # Source directory.
456 redef fun src_path: String do
457 if parent == null then
458 return wiki.config.source_dir
459 else
460 return wiki.expand_path(parent.src_path, name)
461 end
462 end
463
464 # Config
465
466 # Custom configuration file for this section.
467 var config: nullable SectionConfig = null
468
469 # Does this section have its own config file?
470 fun has_config: Bool do return config != null
471
472 # Try to load the config file for this section.
473 private fun try_load_config do
474 var cfile = wiki.expand_path(wiki.config.root_dir, src_path, wiki.config_filename)
475 if not cfile.file_exists then return
476 wiki.message("Custom config for section {name}", 1)
477 config = new SectionConfig(cfile)
478 end
479
480 # Templating
481
482 # Also check custom config.
483 redef fun template_file do
484 if has_config then
485 var tpl = config.template_file
486 if tpl != null then return tpl
487 end
488 if is_root then return wiki.config.template_file
489 return parent.template_file
490 end
491
492 # Also check custom config.
493 redef fun header_file do
494 if has_config then
495 var tpl = config.header_file
496 if tpl != null then return tpl
497 end
498 if is_root then return wiki.config.header_file
499 return parent.header_file
500 end
501
502 # Also check custom config.
503 redef fun footer_file do
504 if has_config then
505 var tpl = config.footer_file
506 if tpl != null then return tpl
507 end
508 if is_root then return wiki.config.footer_file
509 return parent.footer_file
510 end
511
512 # Also check custom config.
513 redef fun menu_file do
514 if has_config then
515 var tpl = config.menu_file
516 if tpl != null then return tpl
517 end
518 if is_root then return wiki.config.menu_file
519 return parent.menu_file
520 end
521 end
522
523 # Each WikiArticle is related to a HTML file.
524 #
525 # Article can be created from scratch using this API or
526 # automatically from a markdown source file (see: `from_source`).
527 class WikiArticle
528 super WikiEntry
529
530 # Articles can only have `WikiSection` as parents.
531 redef type PARENT: WikiSection
532
533 redef fun title: String do
534 if name == "index" and parent != null then return parent.title
535 return super
536 end
537
538 # Page content.
539 #
540 # What you want to be displayed in the page.
541 var content: nullable Writable = null is writable
542
543 # Create a new article using a markdown source file.
544 init from_source(wiki: Nitiwiki, md_file: String) do
545 src_full_path = md_file
546 init(wiki, md_file.basename(".{wiki.config.md_ext}"))
547 content = md
548 end
549
550 redef var src_full_path: nullable String = null
551
552 redef fun src_path do
553 var src_full_path = self.src_full_path
554 if src_full_path == null then return null
555 var res = wiki.config.root_dir.relpath(src_full_path)
556 return res
557 end
558
559 # The page markdown source content.
560 #
561 # Extract the markdown text from `source_file`.
562 #
563 # REQUIRE: `has_source`.
564 var md: nullable String is lazy do
565 if not has_source then return null
566 var file = new FileReader.open(src_full_path.to_s)
567 var md = file.read_all
568 file.close
569 return md
570 end
571
572 # Returns true if has source and
573 # `last_edit_date` > 'last_render_date'.
574 redef fun is_dirty do
575 if super then return true
576 if has_source then
577 return wiki.need_render(src_full_path.to_s, out_full_path)
578 end
579 return false
580 end
581
582 redef fun to_s do return "{name} ({parent or else "null"})"
583 end
584
585 # The sidebar is displayed in front of the main panel of a `WikiEntry`.
586 class WikiSidebar
587
588 # Wiki used to parse sidebar blocks.
589 var wiki: Nitiwiki is lazy do return entry.wiki
590
591 # WikiEntry this panel is related to.
592 var entry: WikiEntry
593
594 # Blocks are ieces of markdown that will be rendered in the sidebar.
595 var blocks: Array[Text] is lazy do
596 var res = new Array[Text]
597 # TODO get blocks from the entry for more customization
598 for name in entry.wiki.config.sidebar_blocks do
599 var block = wiki.load_sideblock(name)
600 if block == null then continue
601 res.add block
602 end
603 return res
604 end
605 end
606
607 # Wiki configuration class.
608 #
609 # This class provides services that ensure static typing when accessing the `config.ini` file.
610 class WikiConfig
611 super ConfigTree
612
613 # Returns the config value at `key` or return `default` if no key was found.
614 private fun value_or_default(key: String, default: String): String do
615 if not has_key(key) then return default
616 return self[key]
617 end
618
619 # Site name displayed.
620 #
621 # The title is used as home title and in headers.
622 #
623 # * key: `wiki.name`
624 # * default: `MyWiki`
625 var wiki_name: String is lazy do return value_or_default("wiki.name", "MyWiki")
626
627 # Site description.
628 #
629 # Displayed in header.
630 #
631 # * key: `wiki.desc`
632 # * default: ``
633 var wiki_desc: String is lazy do return value_or_default("wiki.desc", "")
634
635 # Site logo url.
636 #
637 # Url of the image to be displayed in header.
638 #
639 # * key: `wiki.logo`
640 # * default: ``
641 var wiki_logo: String is lazy do return value_or_default("wiki.logo", "")
642
643 # Markdown extension recognized by this wiki.
644 #
645 # We allow only one kind of extension per wiki.
646 # Files with other markdown extensions will be treated as resources.
647 #
648 # * key: `wiki.md_ext`
649 # * default: `md`
650 var md_ext: String is lazy do return value_or_default("wiki.md_ext", "md")
651
652 # Root directory of the wiki.
653 #
654 # Directory where the wiki files are stored locally.
655 #
656 # * key: `wiki.root_dir`
657 # * default: `./`
658 var root_dir: String is lazy do return value_or_default("wiki.root_dir", "./").simplify_path
659
660 # Pages directory.
661 #
662 # Directory where markdown source files are stored.
663 #
664 # * key: `wiki.source_dir
665 # * default: `pages/`
666 var source_dir: String is lazy do
667 return value_or_default("wiki.source_dir", "pages/").simplify_path
668 end
669
670 # Output directory.
671 #
672 # Directory where public wiki files are generated.
673 # **This path MUST be relative to `root_dir`.**
674 #
675 # * key: `wiki.out_dir`
676 # * default: `out/`
677 var out_dir: String is lazy do return value_or_default("wiki.out_dir", "out/").simplify_path
678
679 # Asset files directory.
680 #
681 # Directory where public assets like JS scripts or CSS files are stored.
682 # **This path MUST be relative to `root_dir`.**
683 #
684 # * key: `wiki.assets_dir`
685 # * default: `assets/`
686 var assets_dir: String is lazy do
687 return value_or_default("wiki.assets_dir", "assets/").simplify_path
688 end
689
690 # Template files directory.
691 #
692 # Directory where template used in HTML generation are stored.
693 # **This path MUST be relative to `root_dir`.**
694 #
695 # * key: `wiki.templates_dir`
696 # * default: `templates/`
697 var templates_dir: String is lazy do
698 return value_or_default("wiki.templates_dir", "templates/").simplify_path
699 end
700
701 # Main template file.
702 #
703 # The main template is used to specify the overall structure of a page.
704 #
705 # * key: `wiki.template`
706 # * default: `template.html`
707 var template_file: String is lazy do
708 return value_or_default("wiki.template", "template.html")
709 end
710
711 # Main header template file.
712 #
713 # Used to specify the structure of the page header.
714 # This is generally the place where you want to put your logo and wiki title.
715 #
716 # * key: `wiki.header`
717 # * default: `header.html`
718 var header_file: String is lazy do
719 return value_or_default("wiki.header", "header.html")
720 end
721
722 # Main menu template file.
723 #
724 # Used to specify the menu structure.
725 #
726 # * key: `wiki.menu`
727 # * default: `menu.html`
728 var menu_file: String is lazy do
729 return value_or_default("wiki.menu", "menu.html")
730 end
731
732 # Main footer file.
733 #
734 # The main footer is used to specify the structure of the page footer.
735 # This is generally the place where you want to put your copyright.
736 #
737 # * key: `wiki.footer`
738 # * default: `footer.html`
739 var footer_file: String is lazy do
740 return value_or_default("wiki.footer", "footer.html")
741 end
742
743 # Automatically add a summary.
744 #
745 # * key: `wiki.auto_summary`
746 # * default: `true`
747 var auto_summary: Bool is lazy do
748 return value_or_default("wiki.auto_summary", "true") == "true"
749 end
750
751 # Automatically add breadcrumbs.
752 #
753 # * key: `wiki.auto_breadcrumbs`
754 # * default: `true`
755 var auto_breadcrumbs: Bool is lazy do
756 return value_or_default("wiki.auto_breadcrumbs", "true") == "true"
757 end
758
759 # Sidebar position.
760 #
761 # Position of the sidebar between `left`, `right` and `none`. Any other value
762 # will be considered as `none`.
763 #
764 # * key: `wiki.sidebar`
765 # * default: `left`
766 var sidebar: String is lazy do
767 return value_or_default("wiki.sidebar", "left")
768 end
769
770 # Sidebar markdown block to include.
771 #
772 # Blocks are specified by their filename without the extension.
773 #
774 # * key: `wiki.sidebar.blocks`
775 # * default: `[]`
776 var sidebar_blocks: Array[String] is lazy do
777 var res = new Array[String]
778 if not has_key("wiki.sidebar.blocks") then return res
779 for val in at("wiki.sidebar.blocks").values do
780 res.add val
781 end
782 return res
783 end
784
785 # Sidebar files directory.
786 #
787 # Directory where sidebar blocks are stored.
788 # **This path MUST be relative to `root_dir`.**
789 #
790 # * key: `wiki.sidebar_dir`
791 # * default: `sidebar/`
792 var sidebar_dir: String is lazy do
793 return value_or_default("wiki.sidebar_dir", "sidebar/").simplify_path
794 end
795
796 # Directory used by rsync to upload wiki files.
797 #
798 # This information is used to update your distant wiki files (like the webserver).
799 #
800 # * key: `wiki.rsync_dir`
801 # * default: ``
802 var rsync_dir: String is lazy do return value_or_default("wiki.rsync_dir", "")
803
804 # Remote repository used to pull modifications on sources.
805 #
806 # * key: `wiki.git_origin`
807 # * default: `origin`
808 var git_origin: String is lazy do return value_or_default("wiki.git_origin", "origin")
809
810 # Remote branch used to pull modifications on sources.
811 #
812 # * key: `wiki.git_branch`
813 # * default: `master`
814 var git_branch: String is lazy do return value_or_default("wiki.git_branch", "master")
815
816 # URL to source versionning used to display last changes
817 #
818 # * key: `wiki.last_changes`
819 # * default: ``
820 var last_changes: String is lazy do return value_or_default("wiki.last_changes", "")
821
822 # URL to source edition.
823 #
824 # * key: `wiki.edit`
825 # * default: ``
826 var edit: String is lazy do return value_or_default("wiki.edit", "")
827 end
828
829 # WikiSection custom configuration.
830 #
831 # Each section can provide its own config file to customize
832 # appearance or behavior.
833 class SectionConfig
834 super ConfigTree
835
836 # Returns the config value at `key` or `null` if no key was found.
837 private fun value_or_null(key: String): nullable String do
838 if not has_key(key) then return null
839 return self[key]
840 end
841
842 # Is this section hidden in sitemap and trees and menus?
843 fun is_hidden: Bool do return value_or_null("section.hidden") == "true"
844
845 # Custom section title if any.
846 fun title: nullable String do return value_or_null("section.title")
847
848 # Custom template file if any.
849 fun template_file: nullable String do return value_or_null("section.template")
850
851 # Custom header file if any.
852 fun header_file: nullable String do return value_or_null("section.header")
853
854 # Custom menu file if any.
855 fun menu_file: nullable String do return value_or_null("section.menu")
856
857 # Custom footer file if any.
858 fun footer_file: nullable String do return value_or_null("section.footer")
859 end