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