1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # Base entities of a nitiwiki.
18 import template
::macro
23 # A Nitiwiki instance.
25 # Nitiwiki provide all base services used by `WikiSection` and `WikiArticle`.
26 # It manages content and renders pages.
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.
34 var config
: WikiConfig
36 # Default config filename.
37 var config_filename
= "config.ini"
39 # Force render on all file even if the source is unmodified.
40 var force_render
= false is writable
43 var verbose_level
= 0 is writable
45 # Delete all the output files.
47 var out_dir
= expand_path
(config
.root_dir
, config
.out_dir
)
48 if out_dir
.file_exists
then out_dir
.rmdir
51 # Synchronize local output with the distant `WikiConfig::rsync_dir`.
53 var root
= expand_path
(config
.root_dir
, config
.out_dir
)
54 sys
.system
"rsync -vr --delete {root}/ {config.rsync_dir}"
57 # Pull data from git repository.
59 sys
.system
"git pull {config.git_origin} {config.git_branch}"
62 # Analyze wiki files from `dir` to build wiki entries.
64 # This method build a hierarchical structure of `WikiSection` and `WikiArticle`
65 # based on the markdown source structure.
67 var dir
= expand_path
(config
.root_dir
, config
.source_dir
)
68 root_section
= new_section
(dir
)
69 var files
= list_md_files
(dir
)
81 print
"name: {config.wiki_name}"
82 print
"config: {config.ini_file}"
83 print
"url: {config.root_url}"
85 if root_section
.is_dirty
then
86 print
"There is modified files:"
87 var paths
= entries
.keys
.to_a
88 var s
= new DefaultComparator
91 var entry
= entries
[path
]
92 if not entry
.is_dirty
then continue
94 if entry
.has_source
then name
= entry
.src_path
.to_s
102 print
"Use nitiwiki --render to render modified files"
104 print
"Wiki is up-to-date"
106 print
"Use nitiwiki --fetch to pull modification from origin"
107 print
"Use nitiwiki --rsync to synchronize distant output"
111 # Display msg if `level <= verbose_level`
112 fun message
(msg
: String, level
: Int) do
113 if level
<= verbose_level
then print msg
116 # List markdown source files from a directory.
117 fun list_md_files
(dir
: String): Array[String] do
118 var files
= new Array[String]
119 var pipe
= new ProcessReader("find", dir
, "-name", "*.md")
120 while not pipe
.eof
do
121 var file
= pipe
.read_line
122 if file
== "" then break # last line
123 var name
= file
.basename
(".md")
124 if name
== "header" or name
== "footer" or name
== "menu" then continue
129 if pipe
.status
!= 0 then exit
1
130 var s
= new DefaultComparator
135 # Does `src` have been modified since `target` creation?
137 # Always returns `true` if `--force` is on.
138 fun need_render
(src
, target
: String): Bool do
139 if force_render
then return true
140 if not target
.file_exists
then return true
141 return src
.file_stat
.mtime
>= target
.file_stat
.mtime
144 # Create a new `WikiSection`.
146 # `path` is used to determine the place in the wiki hierarchy.
147 protected fun new_section
(path
: String): WikiSection do
148 path
= path
.simplify_path
149 if entries
.has_key
(path
) then return entries
[path
].as(WikiSection)
150 var root
= expand_path
(config
.root_dir
, config
.source_dir
)
151 var name
= path
.basename
("")
152 var section
= new WikiSection(self, name
)
153 entries
[path
] = section
154 if path
== root
then return section
155 var ppath
= path
.dirname
156 if ppath
!= path
then
157 var parent
= new_section
(ppath
)
158 parent
.add_child
(section
)
160 section
.try_load_config
164 # Create a new `WikiArticle`.
166 # `path` is used to determine the ancestor sections.
167 protected fun new_article
(path
: String): WikiArticle do
168 if entries
.has_key
(path
) then return entries
[path
].as(WikiArticle)
169 message
("Found article `{path}`", 2)
170 var article
= new WikiArticle.from_source
(self, path
)
171 var section
= new_section
(path
.dirname
)
172 section
.add_child
(article
)
173 entries
[path
] = article
177 # Wiki entries found in the last `lookup_hierarchy`.
178 var entries
= new HashMap[String, WikiEntry]
180 # The root `WikiSection` of the site found in the last `lookup_hierarchy`.
181 var root_section
: WikiSection is noinit
183 # Does a template named `name` exists for this wiki?
184 fun has_template
(name
: String): Bool do
185 return expand_path
(config
.root_dir
, config
.templates_dir
, name
).file_exists
188 # Load a template file as a `TemplateString`.
190 # REQUIRE: `has_template`
191 fun load_template
(name
: String): TemplateString do
192 assert has_template
(name
)
193 var file
= expand_path
(config
.root_dir
, config
.templates_dir
, name
)
194 var tpl
= new TemplateString.from_file
(file
)
195 if tpl
.has_macro
("ROOT_URL") then
196 tpl
.replace
("ROOT_URL", config
.root_url
)
198 if tpl
.has_macro
("TITLE") then
199 tpl
.replace
("TITLE", config
.wiki_name
)
201 if tpl
.has_macro
("SUBTITLE") then
202 tpl
.replace
("SUBTITLE", config
.wiki_desc
)
204 if tpl
.has_macro
("LOGO") then
205 tpl
.replace
("LOGO", config
.wiki_logo
)
210 # Join `parts` as a path and simplify it
211 fun expand_path
(parts
: String...): String do
214 path
= path
.join_path
(part
)
216 return path
.simplify_path
219 # Transform an id style name into a pretty printed name.
221 # Used to translate ids in beautiful page names.
222 fun pretty_name
(name
: String): String do
223 name
= name
.replace
("_", " ")
224 name
= name
.capitalized
229 # A wiki is composed of hierarchical entries.
230 abstract class WikiEntry
232 # `Nitiwiki` this entry belongs to.
237 # Entry internal name.
239 # Mainly used in urls.
242 # Displayed title for `self`.
244 # If `self` is the root entry then display the wiki `WikiConfig::wiki_name` instead.
246 if is_root
then return wiki
.config
.wiki_name
247 return wiki
.pretty_name
(name
)
250 # Is this section rendered from a source document?
252 # Source is an abstract concept at this level.
253 # It can represent a directory, a source file,
254 # a part of a file, everything needed to
255 # extend this base framework.
256 fun has_source
: Bool do return src_path
!= null
258 # Entry creation time.
260 # Returns `-1` if not `has_source`.
261 fun create_time
: Int do
262 if not has_source
then return -1
263 return src_full_path
.file_stat
.ctime
266 # Entry last modification time.
268 # Returns `-1` if not `has_source`.
269 fun last_edit_time
: Int do
270 if not has_source
then return -1
271 return src_full_path
.file_stat
.mtime
274 # Entry list rendering time.
276 # Returns `-1` if `is_new`.
277 fun last_render_time
: Int do
278 if is_new
then return -1
279 return out_full_path
.file_stat
.mtime
284 # Type of the parent entry.
285 type PARENT: WikiEntry
287 # Parent entry if any.
288 var parent
: nullable PARENT = null
290 # Does `self` have a parent?
291 fun is_root
: Bool do return parent
== null
293 # Children labelled by `name`.
294 var children
= new HashMap[String, WikiEntry]
296 # Does `self` have a child nammed `name`?
297 fun has_child
(name
: String): Bool do return children
.keys
.has
(name
)
299 # Retrieve the child called `name`.
300 fun child
(name
: String): WikiEntry do return children
[name
]
302 # Add a sub-entry to `self`.
303 fun add_child
(entry
: WikiEntry) do
305 children
[entry
.name
] = entry
310 # Breadcrumbs from the `Nitiwiki::root_section` to `self`.
312 # Result is returned as an array containg ordered entries:
313 # `breadcrumbs.first` is the root entry and
314 # `breadcrumbs.last == self`
315 var breadcrumbs
: Array[WikiEntry] is lazy
do
316 var path
= new Array[WikiEntry]
317 var entry
: nullable WikiEntry = self
318 while entry
!= null and not entry
.is_root
do
325 # Relative path from `wiki.config.root_dir` to source if any.
326 fun src_path
: nullable String is abstract
328 # Absolute path to the source if any.
329 fun src_full_path
: nullable String do
331 if src
== null then return null
332 return wiki
.config
.root_dir
.join_path
(src
)
335 # Relative path from `wiki.config.root_dir` to rendered output.
337 # Like `src_path`, this method can represent a
338 # directory or a file.
339 fun out_path
: String is abstract
341 # Absolute path to the output.
342 fun out_full_path
: String do return wiki
.config
.root_dir
.join_path
(out_path
)
346 # Does `self` have already been rendered?
347 fun is_new
: Bool do return not out_full_path
.file_exists
349 # Does `self` rendered output is outdated?
351 # Returns `true` if `is_new` then check in children.
352 fun is_dirty
: Bool do
353 if is_new
then return true
355 if last_edit_time
>= last_render_time
then return true
357 for child
in children
.values
do
358 if child
.is_dirty
then return true
363 # Render `self` and `children` is needed.
364 fun render
do for child
in children
.values
do child
.render
368 # Template file for `self`.
370 # Each entity can use a custom template.
371 # By default the template is inherited from the parent.
373 # If the root does not have a custom template,
374 # then returns the main wiki template file.
375 fun template_file
: String do
376 if is_root
then return wiki
.config
.template_file
377 return parent
.template_file
380 # Header template file for `self`.
382 # Behave like `template_file`.
383 fun header_file
: String do
384 if is_root
then return wiki
.config
.header_file
385 return parent
.header_file
388 # Footer template file for `self`.
390 # Behave like `template_file`.
391 fun footer_file
: String do
392 if is_root
then return wiki
.config
.footer_file
393 return parent
.footer_file
396 # Menu template file for `self`.
398 # Behave like `template_file`.
399 fun menu_file
: String do
400 if is_root
then return wiki
.config
.menu_file
401 return parent
.menu_file
404 # Display the entry `name`.
405 redef fun to_s
do return name
408 # Each WikiSection is related to a source directory.
410 # A section can contain other sub-sections or pages.
414 # A section can only have another section as parent.
415 redef type PARENT: WikiSection
419 var title
= config
.title
420 if title
!= null then return title
425 # Is this section hidden?
427 # Hidden section are rendered but not linked in menus.
428 fun is_hidden
: Bool do
429 if has_config
then return config
.is_hidden
434 redef fun src_path
: String do
435 if parent
== null then
436 return wiki
.config
.source_dir
438 return wiki
.expand_path
(parent
.src_path
, name
)
444 # Custom configuration file for this section.
445 var config
: nullable SectionConfig = null
447 # Does this section have its own config file?
448 fun has_config
: Bool do return config
!= null
450 # Try to load the config file for this section.
451 private fun try_load_config
do
452 var cfile
= wiki
.expand_path
(wiki
.config
.root_dir
, src_path
, wiki
.config_filename
)
453 if not cfile
.file_exists
then return
454 wiki
.message
("Custom config for section {name}", 1)
455 config
= new SectionConfig(cfile
)
460 # Also check custom config.
461 redef fun template_file
do
463 var tpl
= config
.template_file
464 if tpl
!= null then return tpl
466 if is_root
then return wiki
.config
.template_file
467 return parent
.template_file
470 # Also check custom config.
471 redef fun header_file
do
473 var tpl
= config
.header_file
474 if tpl
!= null then return tpl
476 if is_root
then return wiki
.config
.header_file
477 return parent
.header_file
480 # Also check custom config.
481 redef fun footer_file
do
483 var tpl
= config
.footer_file
484 if tpl
!= null then return tpl
486 if is_root
then return wiki
.config
.footer_file
487 return parent
.footer_file
490 # Also check custom config.
491 redef fun menu_file
do
493 var tpl
= config
.menu_file
494 if tpl
!= null then return tpl
496 if is_root
then return wiki
.config
.menu_file
497 return parent
.menu_file
501 # Each WikiArticle is related to a HTML file.
503 # Article can be created from scratch using this API or
504 # automatically from a markdown source file (see: `from_source`).
508 # Articles can only have `WikiSection` as parents.
509 redef type PARENT: WikiSection
511 redef fun title
: String do
512 if name
== "index" and parent
!= null then return parent
.title
518 # What you want to be displayed in the page.
519 var content
: nullable Writable = null
521 # Headlines ids and titles.
522 var headlines
= new ArrayMap[String, HeadLine]
524 # Create a new articleu sing a markdown source file.
525 init from_source
(wiki
: Nitiwiki, md_file
: String) do
526 src_full_path
= md_file
527 init(wiki
, md_file
.basename
(".md"))
528 var md_proc
= new MarkdownProcessor
529 content
= md_proc
.process
(md
)
530 headlines
= md_proc
.emitter
.decorator
.headlines
533 redef var src_full_path
: nullable String = null
535 redef fun src_path
do
536 if src_full_path
== null then return null
537 return src_full_path
.substring_from
(wiki
.config
.root_dir
.length
)
540 # The page markdown source content.
542 # Extract the markdown text from `source_file`.
544 # REQUIRE: `has_source`.
545 var md
: nullable String is lazy
do
546 if not has_source
then return null
547 var file
= new FileReader.open
(src_full_path
.to_s
)
548 var md
= file
.read_all
553 # Returns true if has source and
554 # `last_edit_date` > 'last_render_date'.
555 redef fun is_dirty
do
556 if super then return true
558 return wiki
.need_render
(src_full_path
.to_s
, out_full_path
)
563 redef fun to_s
do return "{name} ({parent or else "null"})"
566 # Wiki configuration class.
568 # This class provides services that ensure static typing when accessing the `config.ini` file.
572 # Returns the config value at `key` or return `default` if no key was found.
573 private fun value_or_default
(key
: String, default
: String): String do
574 if not has_key
(key
) then return default
578 # Site name displayed.
580 # The title is used as home title and in headers.
583 # * default: `MyWiki`
584 var wiki_name
: String is lazy
do return value_or_default
("wiki.name", "MyWiki")
588 # Displayed in header.
592 var wiki_desc
: String is lazy
do return value_or_default
("wiki.desc", "")
596 # Url of the image to be displayed in header.
600 var wiki_logo
: String is lazy
do return value_or_default
("wiki.logo", "")
602 # Root url of the wiki.
604 # * key: `wiki.root_url`
605 # * default: `http://localhost/`
606 var root_url
: String is lazy
do return value_or_default
("wiki.root_url", "http://localhost/")
609 # Root directory of the wiki.
611 # Directory where the wiki files are stored locally.
613 # * key: `wiki.root_dir`
615 var root_dir
: String is lazy
do return value_or_default
("wiki.root_dir", "./").simplify_path
619 # Directory where markdown source files are stored.
621 # * key: `wiki.source_dir
622 # * default: `pages/`
623 var source_dir
: String is lazy
do
624 return value_or_default
("wiki.source_dir", "pages/").simplify_path
629 # Directory where public wiki files are generated.
630 # **This path MUST be relative to `root_dir`.**
632 # * key: `wiki.out_dir`
634 var out_dir
: String is lazy
do return value_or_default
("wiki.out_dir", "out/").simplify_path
636 # Asset files directory.
638 # Directory where public assets like JS scripts or CSS files are stored.
639 # **This path MUST be relative to `root_dir`.**
641 # * key: `wiki.assets_dir`
642 # * default: `assets/`
643 var assets_dir
: String is lazy
do
644 return value_or_default
("wiki.assets_dir", "assets/").simplify_path
647 # Template files directory.
649 # Directory where template used in HTML generation are stored.
650 # **This path MUST be relative to `root_dir`.**
652 # * key: `wiki.templates_dir`
653 # * default: `templates/`
654 var templates_dir
: String is lazy
do
655 return value_or_default
("wiki.templates_dir", "templates/").simplify_path
658 # Main template file.
660 # The main template is used to specify the overall structure of a page.
662 # * key: `wiki.template`
663 # * default: `template.html`
664 var template_file
: String is lazy
do
665 return value_or_default
("wiki.template", "template.html")
668 # Main header template file.
670 # Used to specify the structure of the page header.
671 # This is generally the place where you want to put your logo and wiki title.
673 # * key: `wiki.header`
674 # * default: `header.html`
675 var header_file
: String is lazy
do
676 return value_or_default
("wiki.header", "header.html")
679 # Main menu template file.
681 # Used to specify the menu structure.
684 # * default: `menu.html`
685 var menu_file
: String is lazy
do
686 return value_or_default
("wiki.menu", "menu.html")
691 # The main footer is used to specify the structure of the page footer.
692 # This is generally the place where you want to put your copyright.
694 # * key: `wiki.footer`
695 # * default: `footer.html`
696 var footer_file
: String is lazy
do
697 return value_or_default
("wiki.footer", "footer.html")
700 # Directory used by rsync to upload wiki files.
702 # This information is used to update your distant wiki files (like the webserver).
704 # * key: `wiki.rsync_dir`
706 var rsync_dir
: String is lazy
do return value_or_default
("wiki.rsync_dir", "")
708 # Remote repository used to pull modifications on sources.
710 # * key: `wiki.git_origin`
711 # * default: `origin`
712 var git_origin
: String is lazy
do return value_or_default
("wiki.git_origin", "origin")
714 # Remote branch used to pull modifications on sources.
716 # * key: `wiki.git_branch`
717 # * default: `master`
718 var git_branch
: String is lazy
do return value_or_default
("wiki.git_branch", "master")
721 # WikiSection custom configuration.
723 # Each section can provide its own config file to customize
724 # appearance or behavior.
728 # Returns the config value at `key` or `null` if no key was found.
729 private fun value_or_null
(key
: String): nullable String do
730 if not has_key
(key
) then return null
734 # Is this section hidden in sitemap and trees and menus?
735 fun is_hidden
: Bool do return value_or_null
("section.hidden") == "true"
737 # Custom section title if any.
738 fun title
: nullable String do return value_or_null
("section.title")
740 # Custom template file if any.
741 fun template_file
: nullable String do return value_or_null
("section.template")
743 # Custom header file if any.
744 fun header_file
: nullable String do return value_or_null
("section.header")
746 # Custom menu file if any.
747 fun menu_file
: nullable String do return value_or_null
("section.menu")
749 # Custom footer file if any.
750 fun footer_file
: nullable String do return value_or_null
("section.footer")