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
22 # A Nitiwiki instance.
24 # Nitiwiki provide all base services used by `WikiSection` and `WikiArticle`.
25 # It manages content and renders pages.
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.
33 var config
: WikiConfig
35 # Default config filename.
36 var config_filename
= "config.ini"
38 # Force render on all file even if the source is unmodified.
39 var force_render
= false is writable
42 var verbose_level
= 0 is writable
44 # Delete all the output files.
46 var out_dir
= expand_path
(config
.root_dir
, config
.out_dir
)
47 if out_dir
.file_exists
then out_dir
.rmdir
50 # Synchronize local output with the distant `WikiConfig::rsync_dir`.
52 var root
= expand_path
(config
.root_dir
, config
.out_dir
)
53 var rsync_dir
= config
.rsync_dir
54 if rsync_dir
== "" then
55 message
("Error: configure `wiki.rsync_dir` to use rsync.", 0)
58 sys
.system
"rsync -vr --delete -- {root.escape_to_sh}/ {rsync_dir.escape_to_sh}"
61 # Pull data from git repository.
63 sys
.system
"git pull {config.git_origin.escape_to_sh} {config.git_branch.escape_to_sh}"
66 # Analyze wiki files from `dir` to build wiki entries.
68 # This method build a hierarchical structure of `WikiSection` and `WikiArticle`
69 # based on the markdown source structure.
71 var dir
= expand_path
(config
.root_dir
, config
.source_dir
)
72 root_section
= new_section
(dir
)
73 var files
= list_md_files
(dir
)
85 print
"name: {config.wiki_name}"
86 print
"config: {config.ini_file}"
88 if root_section
.is_dirty
then
89 print
"There is modified files:"
90 var paths
= entries
.keys
.to_a
91 var s
= new DefaultComparator
94 var entry
= entries
[path
]
95 if not entry
.is_dirty
then continue
97 if entry
.has_source
then name
= entry
.src_path
.as(not null)
105 print
"Use nitiwiki --render to render modified files"
107 print
"Wiki is up-to-date"
109 print
"Use nitiwiki --fetch to pull modification from origin"
110 print
"Use nitiwiki --rsync to synchronize distant output"
114 # Display msg if `level <= verbose_level`
115 fun message
(msg
: String, level
: Int) do
116 if level
<= verbose_level
then print msg
119 # List markdown source files from a directory.
120 fun list_md_files
(dir
: String): Array[String] do
121 var files
= new Array[String]
122 var pipe
= new ProcessReader("find", dir
, "-name", "*.{config.md_ext}")
123 while not pipe
.eof
do
124 var file
= pipe
.read_line
125 if file
== "" then break # last line
126 var name
= file
.basename
(".{config.md_ext}")
127 if name
== "header" or name
== "footer" or name
== "menu" then continue
132 if pipe
.status
!= 0 then exit
1
133 var s
= new DefaultComparator
138 # Does `src` have been modified since `target` creation?
140 # Always returns `true` if `--force` is on.
141 fun need_render
(src
, target
: String): Bool do
142 if force_render
then return true
143 if not target
.file_exists
then return true
144 return src
.file_stat
.as(not null).mtime
>= target
.file_stat
.as(not null).mtime
147 # Create a new `WikiSection`.
149 # `path` is used to determine the place in the wiki hierarchy.
150 protected fun new_section
(path
: String): WikiSection do
151 path
= path
.simplify_path
152 if entries
.has_key
(path
) then return entries
[path
].as(WikiSection)
153 var root
= expand_path
(config
.root_dir
, config
.source_dir
)
154 var name
= path
.basename
155 var section
= new WikiSection(self, name
)
156 entries
[path
] = section
157 if path
== root
then return section
158 var ppath
= path
.dirname
159 if ppath
!= path
then
160 var parent
= new_section
(ppath
)
161 parent
.add_child
(section
)
163 section
.try_load_config
167 # Create a new `WikiArticle`.
169 # `path` is used to determine the ancestor sections.
170 protected fun new_article
(path
: String): WikiArticle do
171 if entries
.has_key
(path
) then return entries
[path
].as(WikiArticle)
172 message
("Found article `{path}`", 2)
173 var article
= new WikiArticle.from_source
(self, path
)
174 var section
= new_section
(path
.dirname
)
175 section
.add_child
(article
)
176 entries
[path
] = article
180 # Wiki entries found in the last `lookup_hierarchy`.
181 var entries
= new HashMap[String, WikiEntry]
183 # The root `WikiSection` of the site found in the last `lookup_hierarchy`.
184 var root_section
: WikiSection is noinit
186 # Does a template named `name` exists for this wiki?
187 fun has_template
(name
: String): Bool do
188 return expand_path
(config
.root_dir
, config
.templates_dir
, name
).file_exists
191 # Load a template file as a `TemplateString`.
193 # REQUIRE: `has_template`
194 fun load_template
(name
: String): TemplateString do
195 if not has_template
(name
) then
196 message
("Error: can't load template `{name}`", 0)
199 var file
= expand_path
(config
.root_dir
, config
.templates_dir
, name
)
200 var tpl
= new TemplateString.from_file
(file
)
201 if tpl
.has_macro
("TITLE") then
202 tpl
.replace
("TITLE", config
.wiki_name
)
204 if tpl
.has_macro
("SUBTITLE") then
205 tpl
.replace
("SUBTITLE", config
.wiki_desc
)
207 if tpl
.has_macro
("LOGO") then
208 tpl
.replace
("LOGO", config
.wiki_logo
)
213 # Does a sideblock named `name` exists for this wiki?
214 fun has_sideblock
(name
: String): Bool do
215 name
= "{name}.{config.md_ext}"
216 return expand_path
(config
.root_dir
, config
.sidebar_dir
, name
).file_exists
219 # Load a markdown block with `name` from `WikiConfig::sidebar_dir`.
220 private fun load_sideblock
(name
: String): nullable String do
221 if not has_sideblock
(name
) then
222 message
("Error: can't load sideblock `{name}`", 0)
225 name
= "{name}.{config.md_ext}"
226 var path
= expand_path
(config
.root_dir
, config
.sidebar_dir
, name
)
227 var file
= new FileReader.open
(path
)
228 var res
= file
.read_all
233 # Join `parts` as a path and simplify it
234 fun expand_path
(parts
: String...): String do
237 path
= path
.join_path
(part
)
239 return path
.simplify_path
242 # Transform an id style name into a pretty printed name.
244 # Used to translate ids in beautiful page names.
245 fun pretty_name
(name
: String): String do
246 name
= name
.replace
("_", " ")
247 name
= name
.capitalized
(keep_upper
=true)
252 # A wiki is composed of hierarchical entries.
253 abstract class WikiEntry
255 # `Nitiwiki` this entry belongs to.
260 # Entry internal name.
262 # Mainly used in urls.
265 # Displayed title for `self`.
267 # If `self` is the root entry then display the wiki `WikiConfig::wiki_name` instead.
269 if is_root
then return wiki
.config
.wiki_name
270 return wiki
.pretty_name
(name
)
273 # Is this section rendered from a source document?
275 # Source is an abstract concept at this level.
276 # It can represent a directory, a source file,
277 # a part of a file, everything needed to
278 # extend this base framework.
279 fun has_source
: Bool do return src_path
!= null
281 # Entry creation time.
283 # Returns `-1` if not `has_source`.
284 fun create_time
: Int do
285 if not has_source
then return -1
286 return src_full_path
.as(not null).file_stat
.as(not null).ctime
289 # Entry last modification time.
291 # Returns `-1` if not `has_source`.
292 fun last_edit_time
: Int do
293 if not has_source
then return -1
294 return src_full_path
.as(not null).file_stat
.as(not null).mtime
297 # Entry list rendering time.
299 # Returns `-1` if `is_new`.
300 fun last_render_time
: Int do
301 if is_new
then return -1
302 return out_full_path
.file_stat
.as(not null).mtime
307 # Type of the parent entry.
308 type PARENT: WikiEntry
310 # Parent entry if any.
311 var parent
: nullable PARENT = null
313 # Does `self` have a parent?
314 fun is_root
: Bool do return parent
== null
316 # Children labelled by `name`.
317 var children
= new HashMap[String, WikiEntry]
319 # Does `self` have a child nammed `name`?
320 fun has_child
(name
: String): Bool do return children
.keys
.has
(name
)
322 # Retrieve the child called `name`.
323 fun child
(name
: String): WikiEntry do return children
[name
]
325 # Add a sub-entry to `self`.
326 fun add_child
(entry
: WikiEntry) do
328 children
[entry
.name
] = entry
333 # Breadcrumbs from the `Nitiwiki::root_section` to `self`.
335 # Result is returned as an array containg ordered entries:
336 # `breadcrumbs.first` is the root entry and
337 # `breadcrumbs.last == self`
338 var breadcrumbs
: Array[WikiEntry] is lazy
do
339 var path
= new Array[WikiEntry]
340 var entry
: nullable WikiEntry = self
341 while entry
!= null and not entry
.is_root
do
348 # Sidebar relative to this wiki entry.
349 var sidebar
= new WikiSidebar(self)
351 # Relative path from `wiki.config.root_dir` to source if any.
352 fun src_path
: nullable String is abstract
354 # Absolute path to the source if any.
355 fun src_full_path
: nullable String do
357 if src
== null then return null
358 return wiki
.config
.root_dir
.join_path
(src
)
361 # Relative path from `wiki.config.root_dir` to rendered output.
363 # Like `src_path`, this method can represent a
364 # directory or a file.
365 fun out_path
: String is abstract
367 # Absolute path to the output.
368 fun out_full_path
: String do return wiki
.config
.root_dir
.join_path
(out_path
)
372 # Does `self` have already been rendered?
373 fun is_new
: Bool do return not out_full_path
.file_exists
375 # Does `self` rendered output is outdated?
377 # Returns `true` if `is_new` then check in children.
378 fun is_dirty
: Bool do
379 if is_new
then return true
381 if last_edit_time
>= last_render_time
then return true
383 for child
in children
.values
do
384 if child
.is_dirty
then return true
389 # Render `self` and `children` is needed.
390 fun render
do for child
in children
.values
do child
.render
394 # Template file for `self`.
396 # Each entity can use a custom template.
397 # By default the template is inherited from the parent.
399 # If the root does not have a custom template,
400 # then returns the main wiki template file.
401 fun template_file
: String do
402 if is_root
then return wiki
.config
.template_file
403 return parent
.as(not null).template_file
406 # Header template file for `self`.
408 # Behave like `template_file`.
409 fun header_file
: String do
410 if is_root
then return wiki
.config
.header_file
411 return parent
.as(not null).header_file
414 # Footer template file for `self`.
416 # Behave like `template_file`.
417 fun footer_file
: String do
418 if is_root
then return wiki
.config
.footer_file
419 return parent
.as(not null).footer_file
422 # Menu template file for `self`.
424 # Behave like `template_file`.
425 fun menu_file
: String do
426 if is_root
then return wiki
.config
.menu_file
427 return parent
.as(not null).menu_file
430 # Display the entry `name`.
431 redef fun to_s
do return name
434 # Each WikiSection is related to a source directory.
436 # A section can contain other sub-sections or pages.
440 # A section can only have another section as parent.
441 redef type PARENT: WikiSection
445 var title
= config
.as(not null).title
446 if title
!= null then return title
451 # Is this section hidden?
453 # Hidden section are rendered but not linked in menus.
454 fun is_hidden
: Bool do
455 if has_config
then return config
.as(not null).is_hidden
460 redef fun src_path
: String do
461 if parent
== null then
462 return wiki
.config
.source_dir
464 return wiki
.expand_path
(parent
.as(not null).src_path
, name
)
470 # Custom configuration file for this section.
471 var config
: nullable SectionConfig = null
473 # Does this section have its own config file?
474 fun has_config
: Bool do return config
!= null
476 # Try to load the config file for this section.
477 private fun try_load_config
do
478 var cfile
= wiki
.expand_path
(wiki
.config
.root_dir
, src_path
, wiki
.config_filename
)
479 if not cfile
.file_exists
then return
480 wiki
.message
("Custom config for section {name}", 2)
481 config
= new SectionConfig(cfile
)
486 # Also check custom config.
487 redef fun template_file
do
489 var tpl
= config
.as(not null).template_file
490 if tpl
!= null then return tpl
492 if is_root
then return wiki
.config
.template_file
493 return parent
.as(not null).template_file
496 # Also check custom config.
497 redef fun header_file
do
499 var tpl
= config
.as(not null).header_file
500 if tpl
!= null then return tpl
502 if is_root
then return wiki
.config
.header_file
503 return parent
.as(not null).header_file
506 # Also check custom config.
507 redef fun footer_file
do
509 var tpl
= config
.as(not null).footer_file
510 if tpl
!= null then return tpl
512 if is_root
then return wiki
.config
.footer_file
513 return parent
.as(not null).footer_file
516 # Also check custom config.
517 redef fun menu_file
do
519 var tpl
= config
.as(not null).menu_file
520 if tpl
!= null then return tpl
522 if is_root
then return wiki
.config
.menu_file
523 return parent
.as(not null).menu_file
527 # Each WikiArticle is related to a HTML file.
529 # Article can be created from scratch using this API or
530 # automatically from a markdown source file (see: `from_source`).
534 # Articles can only have `WikiSection` as parents.
535 redef type PARENT: WikiSection
538 var parent
= self.parent
539 if name
== "index" and parent
!= null then return parent
.title
545 # What you want to be displayed in the page.
546 var content
: nullable Writable = null is writable
548 # Create a new article using a markdown source file.
549 init from_source
(wiki
: Nitiwiki, md_file
: String) do
550 src_full_path
= md_file
551 init(wiki
, md_file
.basename
(".{wiki.config.md_ext}"))
555 redef var src_full_path
= null
557 redef fun src_path
do
558 var src_full_path
= self.src_full_path
559 if src_full_path
== null then return null
560 var res
= wiki
.config
.root_dir
.relpath
(src_full_path
)
564 # The page markdown source content.
566 # Extract the markdown text from `source_file`.
568 # REQUIRE: `has_source`.
569 var md
: nullable String is lazy
do
570 if not has_source
then return null
571 var file
= new FileReader.open
(src_full_path
.as(not null))
572 var md
= file
.read_all
577 # Returns true if has source and
578 # `last_edit_date` > 'last_render_date'.
579 redef fun is_dirty
do
580 if super then return true
582 return wiki
.need_render
(src_full_path
.as(not null), out_full_path
)
587 redef fun to_s
do return "{name} ({parent or else "null"})"
590 # The sidebar is displayed in front of the main panel of a `WikiEntry`.
593 # Wiki used to parse sidebar blocks.
594 var wiki
: Nitiwiki is lazy
do return entry
.wiki
596 # WikiEntry this panel is related to.
599 # Blocks are ieces of markdown that will be rendered in the sidebar.
600 var blocks
: Array[Text] is lazy
do
601 var res
= new Array[Text]
602 # TODO get blocks from the entry for more customization
603 for name
in entry
.wiki
.config
.sidebar_blocks
do
604 var block
= wiki
.load_sideblock
(name
)
605 if block
== null then continue
612 # Wiki configuration class.
614 # This class provides services that ensure static typing when accessing the `config.ini` file.
618 # Returns the config value at `key` or return `default` if no key was found.
619 protected fun value_or_default
(key
: String, default
: String): String do
620 return self[key
] or else default
623 # Site name displayed.
625 # The title is used as home title and in headers.
628 # * default: `MyWiki`
629 var wiki_name
: String is lazy
do return value_or_default
("wiki.name", "MyWiki")
633 # Displayed in header.
637 var wiki_desc
: String is lazy
do return value_or_default
("wiki.desc", "")
641 # Url of the image to be displayed in header.
645 var wiki_logo
: String is lazy
do return value_or_default
("wiki.logo", "")
647 # Markdown extension recognized by this wiki.
649 # We allow only one kind of extension per wiki.
650 # Files with other markdown extensions will be treated as resources.
652 # * key: `wiki.md_ext`
654 var md_ext
: String is lazy
do return value_or_default
("wiki.md_ext", "md")
656 # Root directory of the wiki.
658 # Directory where the wiki files are stored locally.
660 # * key: `wiki.root_dir`
662 var root_dir
: String is lazy
do return value_or_default
("wiki.root_dir", "./").simplify_path
666 # Directory where markdown source files are stored.
668 # * key: `wiki.source_dir
669 # * default: `pages/`
670 var source_dir
: String is lazy
do
671 return value_or_default
("wiki.source_dir", "pages/").simplify_path
676 # Directory where public wiki files are generated.
677 # **This path MUST be relative to `root_dir`.**
679 # * key: `wiki.out_dir`
681 var out_dir
: String is lazy
do return value_or_default
("wiki.out_dir", "out/").simplify_path
683 # Asset files directory.
685 # Directory where public assets like JS scripts or CSS files are stored.
686 # **This path MUST be relative to `root_dir`.**
688 # * key: `wiki.assets_dir`
689 # * default: `assets/`
690 var assets_dir
: String is lazy
do
691 return value_or_default
("wiki.assets_dir", "assets/").simplify_path
694 # Template files directory.
696 # Directory where template used in HTML generation are stored.
697 # **This path MUST be relative to `root_dir`.**
699 # * key: `wiki.templates_dir`
700 # * default: `templates/`
701 var templates_dir
: String is lazy
do
702 return value_or_default
("wiki.templates_dir", "templates/").simplify_path
705 # Main template file.
707 # The main template is used to specify the overall structure of a page.
709 # * key: `wiki.template`
710 # * default: `template.html`
711 var template_file
: String is lazy
do
712 return value_or_default
("wiki.template", "template.html")
715 # Main header template file.
717 # Used to specify the structure of the page header.
718 # This is generally the place where you want to put your logo and wiki title.
720 # * key: `wiki.header`
721 # * default: `header.html`
722 var header_file
: String is lazy
do
723 return value_or_default
("wiki.header", "header.html")
726 # Main menu template file.
728 # Used to specify the menu structure.
731 # * default: `menu.html`
732 var menu_file
: String is lazy
do
733 return value_or_default
("wiki.menu", "menu.html")
738 # The main footer is used to specify the structure of the page footer.
739 # This is generally the place where you want to put your copyright.
741 # * key: `wiki.footer`
742 # * default: `footer.html`
743 var footer_file
: String is lazy
do
744 return value_or_default
("wiki.footer", "footer.html")
747 # Automatically add a summary.
749 # * key: `wiki.auto_summary`
751 var auto_summary
: Bool is lazy
do
752 return value_or_default
("wiki.auto_summary", "true") == "true"
755 # Automatically add breadcrumbs.
757 # * key: `wiki.auto_breadcrumbs`
759 var auto_breadcrumbs
: Bool is lazy
do
760 return value_or_default
("wiki.auto_breadcrumbs", "true") == "true"
765 # Position of the sidebar between `left`, `right` and `none`. Any other value
766 # will be considered as `none`.
768 # * key: `wiki.sidebar`
770 var sidebar
: String is lazy
do
771 return value_or_default
("wiki.sidebar", "left")
774 # Sidebar markdown block to include.
776 # Blocks are specified by their filename without the extension.
778 # * key: `wiki.sidebar.blocks`
780 var sidebar_blocks
: Array[String] is lazy
do
781 var res
= new Array[String]
782 if not has_key
("wiki.sidebar.blocks") then return res
783 for val
in at
("wiki.sidebar.blocks").as(not null).values
do
789 # Sidebar files directory.
791 # Directory where sidebar blocks are stored.
792 # **This path MUST be relative to `root_dir`.**
794 # * key: `wiki.sidebar_dir`
795 # * default: `sidebar/`
796 var sidebar_dir
: String is lazy
do
797 return value_or_default
("wiki.sidebar_dir", "sidebar/").simplify_path
800 # Directory used by rsync to upload wiki files.
802 # This information is used to update your distant wiki files (like the webserver).
804 # * key: `wiki.rsync_dir`
806 var rsync_dir
: String is lazy
do return value_or_default
("wiki.rsync_dir", "")
808 # Remote repository used to pull modifications on sources.
810 # * key: `wiki.git_origin`
811 # * default: `origin`
812 var git_origin
: String is lazy
do return value_or_default
("wiki.git_origin", "origin")
814 # Remote branch used to pull modifications on sources.
816 # * key: `wiki.git_branch`
817 # * default: `master`
818 var git_branch
: String is lazy
do return value_or_default
("wiki.git_branch", "master")
820 # URL to source versionning used to display last changes
822 # * key: `wiki.last_changes`
824 var last_changes
: String is lazy
do return value_or_default
("wiki.last_changes", "")
826 # URL to source edition.
830 var edit
: String is lazy
do return value_or_default
("wiki.edit", "")
833 # WikiSection custom configuration.
835 # Each section can provide its own config file to customize
836 # appearance or behavior.
840 # Returns the config value at `key` or `null` if no key was found.
841 private fun value_or_null
(key
: String): nullable String do
842 if not has_key
(key
) then return null
846 # Is this section hidden in sitemap and trees and menus?
847 fun is_hidden
: Bool do return value_or_null
("section.hidden") == "true"
849 # Custom section title if any.
850 fun title
: nullable String do return value_or_null
("section.title")
852 # Custom template file if any.
853 fun template_file
: nullable String do return value_or_null
("section.template")
855 # Custom header file if any.
856 fun header_file
: nullable String do return value_or_null
("section.header")
858 # Custom menu file if any.
859 fun menu_file
: nullable String do return value_or_null
("section.menu")
861 # Custom footer file if any.
862 fun footer_file
: nullable String do return value_or_null
("section.footer")