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