Merge: Added contributing guidelines and link from readme
[nit.git] / contrib / nitiwiki / src / wiki_base.nit
index 3dec44f..d3adc26 100644 (file)
@@ -16,7 +16,6 @@
 module wiki_base
 
 import template::macro
-import markdown
 import opts
 import ini
 
@@ -51,12 +50,17 @@ class Nitiwiki
        # Synchronize local output with the distant `WikiConfig::rsync_dir`.
        fun sync do
                var root = expand_path(config.root_dir, config.out_dir)
-               sys.system "rsync -vr --delete {root}/ {config.rsync_dir}"
+               var rsync_dir = config.rsync_dir
+               if rsync_dir == "" then
+                       message("Error: configure `wiki.rsync_dir` to use rsync.", 0)
+                       return
+               end
+               sys.system "rsync -vr --delete -- {root.escape_to_sh}/ {rsync_dir.escape_to_sh}"
        end
 
        # Pull data from git repository.
        fun fetch do
-               sys.system "git pull {config.git_origin} {config.git_branch}"
+               sys.system "git pull {config.git_origin.escape_to_sh} {config.git_branch.escape_to_sh}"
        end
 
        # Analyze wiki files from `dir` to build wiki entries.
@@ -72,12 +76,14 @@ class Nitiwiki
                end
        end
 
+       # Render output.
+       fun render do end
+
        # Show wiki status.
        fun status do
                print "nitiWiki"
                print "name: {config.wiki_name}"
                print "config: {config.ini_file}"
-               print "url: {config.root_url}"
                print ""
                if root_section.is_dirty then
                        print "There is modified files:"
@@ -88,7 +94,7 @@ class Nitiwiki
                                var entry = entries[path]
                                if not entry.is_dirty then continue
                                var name = entry.name
-                               if entry.has_source then name = entry.src_path.to_s
+                               if entry.has_source then name = entry.src_path.as(not null)
                                if entry.is_new then
                                        print " + {name}"
                                else
@@ -105,7 +111,7 @@ class Nitiwiki
                end
        end
 
-       # Display msg if `level >= verbose_level`
+       # Display msg if `level <= verbose_level`
        fun message(msg: String, level: Int) do
                if level <= verbose_level then print msg
        end
@@ -113,11 +119,11 @@ class Nitiwiki
        # List markdown source files from a directory.
        fun list_md_files(dir: String): Array[String] do
                var files = new Array[String]
-               var pipe = new IProcess("find", dir, "-name", "*.md")
+               var pipe = new ProcessReader("find", dir, "-name", "*.{config.md_ext}")
                while not pipe.eof do
                        var file = pipe.read_line
                        if file == "" then break # last line
-                       var name = file.basename(".md")
+                       var name = file.basename(".{config.md_ext}")
                        if name == "header" or name == "footer" or name == "menu" then continue
                        files.add file
                end
@@ -135,7 +141,7 @@ class Nitiwiki
        fun need_render(src, target: String): Bool do
                if force_render then return true
                if not target.file_exists then return true
-               return src.file_stat.mtime >= target.file_stat.mtime
+               return src.file_stat.as(not null).mtime >= target.file_stat.as(not null).mtime
        end
 
        # Create a new `WikiSection`.
@@ -145,7 +151,7 @@ class Nitiwiki
                path = path.simplify_path
                if entries.has_key(path) then return entries[path].as(WikiSection)
                var root = expand_path(config.root_dir, config.source_dir)
-               var name = path.basename("")
+               var name = path.basename
                var section = new WikiSection(self, name)
                entries[path] = section
                if path == root then return section
@@ -163,6 +169,7 @@ class Nitiwiki
        # `path` is used to determine the ancestor sections.
        protected fun new_article(path: String): WikiArticle do
                if entries.has_key(path) then return entries[path].as(WikiArticle)
+               message("Found article `{path}`", 2)
                var article = new WikiArticle.from_source(self, path)
                var section = new_section(path.dirname)
                section.add_child(article)
@@ -185,12 +192,12 @@ class Nitiwiki
        #
        # REQUIRE: `has_template`
        fun load_template(name: String): TemplateString do
-               assert has_template(name)
+               if not has_template(name) then
+                       message("Error: can't load template `{name}`", 0)
+                       exit 1
+               end
                var file = expand_path(config.root_dir, config.templates_dir, name)
                var tpl = new TemplateString.from_file(file)
-               if tpl.has_macro("ROOT_URL") then
-                       tpl.replace("ROOT_URL", config.root_url)
-               end
                if tpl.has_macro("TITLE") then
                        tpl.replace("TITLE", config.wiki_name)
                end
@@ -203,6 +210,26 @@ class Nitiwiki
                return tpl
        end
 
+       # Does a sideblock named `name` exists for this wiki?
+       fun has_sideblock(name: String): Bool do
+               name = "{name}.{config.md_ext}"
+               return expand_path(config.root_dir, config.sidebar_dir, name).file_exists
+       end
+
+       # Load a markdown block with `name` from `WikiConfig::sidebar_dir`.
+       private fun load_sideblock(name: String): nullable String do
+               if not has_sideblock(name) then
+                       message("Error: can't load sideblock `{name}`", 0)
+                       return null
+               end
+               name = "{name}.{config.md_ext}"
+               var path = expand_path(config.root_dir, config.sidebar_dir, name)
+               var file = new FileReader.open(path)
+               var res = file.read_all
+               file.close
+               return res
+       end
+
        # Join `parts` as a path and simplify it
        fun expand_path(parts: String...): String do
                var path = ""
@@ -217,7 +244,7 @@ class Nitiwiki
        # Used to translate ids in beautiful page names.
        fun pretty_name(name: String): String do
                name = name.replace("_", " ")
-               name = name.capitalized
+               name = name.capitalized(keep_upper=true)
                return name
        end
 end
@@ -256,7 +283,7 @@ abstract class WikiEntry
        # Returns `-1` if not `has_source`.
        fun create_time: Int do
                if not has_source then return -1
-               return src_full_path.file_stat.ctime
+               return src_full_path.as(not null).file_stat.as(not null).ctime
        end
 
        # Entry last modification time.
@@ -264,7 +291,7 @@ abstract class WikiEntry
        # Returns `-1` if not `has_source`.
        fun last_edit_time: Int do
                if not has_source then return -1
-               return src_full_path.file_stat.mtime
+               return src_full_path.as(not null).file_stat.as(not null).mtime
        end
 
        # Entry list rendering time.
@@ -272,7 +299,7 @@ abstract class WikiEntry
        # Returns `-1` if `is_new`.
        fun last_render_time: Int do
                if is_new then return -1
-               return out_full_path.file_stat.mtime
+               return out_full_path.file_stat.as(not null).mtime
        end
 
        # Entries hierarchy
@@ -308,7 +335,7 @@ abstract class WikiEntry
        # Result is returned as an array containg ordered entries:
        # `breadcrumbs.first` is the root entry and
        # `breadcrumbs.last == self`
-       fun breadcrumbs: Array[WikiEntry] is cached do
+       var breadcrumbs: Array[WikiEntry] is lazy do
                var path = new Array[WikiEntry]
                var entry: nullable WikiEntry = self
                while entry != null and not entry.is_root do
@@ -318,6 +345,9 @@ abstract class WikiEntry
                return path.reversed
        end
 
+       # Sidebar relative to this wiki entry.
+       var sidebar = new WikiSidebar(self)
+
        # Relative path from `wiki.config.root_dir` to source if any.
        fun src_path: nullable String is abstract
 
@@ -370,7 +400,7 @@ abstract class WikiEntry
        # then returns the main wiki template file.
        fun template_file: String do
                if is_root then return wiki.config.template_file
-               return parent.template_file
+               return parent.as(not null).template_file
        end
 
        # Header template file for `self`.
@@ -378,7 +408,7 @@ abstract class WikiEntry
        # Behave like `template_file`.
        fun header_file: String do
                if is_root then return wiki.config.header_file
-               return parent.header_file
+               return parent.as(not null).header_file
        end
 
        # Footer template file for `self`.
@@ -386,7 +416,7 @@ abstract class WikiEntry
        # Behave like `template_file`.
        fun footer_file: String do
                if is_root then return wiki.config.footer_file
-               return parent.footer_file
+               return parent.as(not null).footer_file
        end
 
        # Menu template file for `self`.
@@ -394,7 +424,7 @@ abstract class WikiEntry
        # Behave like `template_file`.
        fun menu_file: String do
                if is_root then return wiki.config.menu_file
-               return parent.menu_file
+               return parent.as(not null).menu_file
        end
 
        # Display the entry `name`.
@@ -412,7 +442,7 @@ class WikiSection
 
        redef fun title do
                if has_config then
-                       var title = config.title
+                       var title = config.as(not null).title
                        if title != null then return title
                end
                return super
@@ -422,7 +452,7 @@ class WikiSection
        #
        # Hidden section are rendered but not linked in menus.
        fun is_hidden: Bool do
-               if has_config then return config.is_hidden
+               if has_config then return config.as(not null).is_hidden
                return false
        end
 
@@ -431,7 +461,7 @@ class WikiSection
                if parent == null then
                        return wiki.config.source_dir
                else
-                       return wiki.expand_path(parent.src_path, name)
+                       return wiki.expand_path(parent.as(not null).src_path, name)
                end
        end
 
@@ -456,41 +486,41 @@ class WikiSection
        # Also check custom config.
        redef fun template_file do
                if has_config then
-                       var tpl = config.template_file
+                       var tpl = config.as(not null).template_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.template_file
-               return parent.template_file
+               return parent.as(not null).template_file
        end
 
        # Also check custom config.
        redef fun header_file do
                if has_config then
-                       var tpl = config.header_file
+                       var tpl = config.as(not null).header_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.header_file
-               return parent.header_file
+               return parent.as(not null).header_file
        end
 
        # Also check custom config.
        redef fun footer_file do
                if has_config then
-                       var tpl = config.footer_file
+                       var tpl = config.as(not null).footer_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.footer_file
-               return parent.footer_file
+               return parent.as(not null).footer_file
        end
 
        # Also check custom config.
        redef fun menu_file do
                if has_config then
-                       var tpl = config.menu_file
+                       var tpl = config.as(not null).menu_file
                        if tpl != null then return tpl
                end
                if is_root then return wiki.config.menu_file
-               return parent.menu_file
+               return parent.as(not null).menu_file
        end
 end
 
@@ -504,7 +534,8 @@ class WikiArticle
        # Articles can only have `WikiSection` as parents.
        redef type PARENT: WikiSection
 
-       redef fun title: String do
+       redef fun title do
+               var parent = self.parent
                if name == "index" and parent != null then return parent.title
                return super
        end
@@ -512,25 +543,22 @@ class WikiArticle
        # Page content.
        #
        # What you want to be displayed in the page.
-       var content: nullable Streamable = null
-
-       # Headlines ids and titles.
-       var headlines = new ArrayMap[String, HeadLine]
+       var content: nullable Writable = null is writable
 
-       # Create a new articleu sing a markdown source file.
+       # Create a new article using a markdown source file.
        init from_source(wiki: Nitiwiki, md_file: String) do
                src_full_path = md_file
-               init(wiki, md_file.basename(".md"))
-               var md_proc = new MarkdownProcessor
-               content = md_proc.process(md)
-               headlines = md_proc.emitter.decorator.headlines
+               init(wiki, md_file.basename(".{wiki.config.md_ext}"))
+               content = md
        end
 
-       redef var src_full_path: nullable String = null
+       redef var src_full_path = null
 
        redef fun src_path do
+               var src_full_path = self.src_full_path
                if src_full_path == null then return null
-               return src_full_path.substring_from(wiki.config.root_dir.length)
+               var res = wiki.config.root_dir.relpath(src_full_path)
+               return res
        end
 
        # The page markdown source content.
@@ -538,9 +566,9 @@ class WikiArticle
        # Extract the markdown text from `source_file`.
        #
        # REQUIRE: `has_source`.
-       fun md: String is cached do
-               assert has_source
-               var file = new IFStream.open(src_full_path.to_s)
+       var md: nullable String is lazy do
+               if not has_source then return null
+               var file = new FileReader.open(src_full_path.as(not null))
                var md = file.read_all
                file.close
                return md
@@ -551,7 +579,7 @@ class WikiArticle
        redef fun is_dirty do
                if super then return true
                if has_source then
-                       return wiki.need_render(src_full_path.to_s, out_full_path)
+                       return wiki.need_render(src_full_path.as(not null), out_full_path)
                end
                return false
        end
@@ -559,6 +587,28 @@ class WikiArticle
        redef fun to_s do return "{name} ({parent or else "null"})"
 end
 
+# The sidebar is displayed in front of the main panel of a `WikiEntry`.
+class WikiSidebar
+
+       # Wiki used to parse sidebar blocks.
+       var wiki: Nitiwiki is lazy do return entry.wiki
+
+       # WikiEntry this panel is related to.
+       var entry: WikiEntry
+
+       # Blocks are ieces of markdown that will be rendered in the sidebar.
+       var blocks: Array[Text] is lazy do
+               var res = new Array[Text]
+               # TODO get blocks from the entry for more customization
+               for name in entry.wiki.config.sidebar_blocks do
+                       var block = wiki.load_sideblock(name)
+                       if block == null then continue
+                       res.add block
+               end
+               return res
+       end
+end
+
 # Wiki configuration class.
 #
 # This class provides services that ensure static typing when accessing the `config.ini` file.
@@ -566,9 +616,8 @@ class WikiConfig
        super ConfigTree
 
        # Returns the config value at `key` or return `default` if no key was found.
-       private fun value_or_default(key: String, default: String): String do
-               if not has_key(key) then return default
-               return self[key]
+       protected fun value_or_default(key: String, default: String): String do
+               return self[key] or else default
        end
 
        # Site name displayed.
@@ -577,7 +626,7 @@ class WikiConfig
        #
        # * key: `wiki.name`
        # * default: `MyWiki`
-       fun wiki_name: String is cached do return value_or_default("wiki.name", "MyWiki")
+       var wiki_name: String is lazy do return value_or_default("wiki.name", "MyWiki")
 
        # Site description.
        #
@@ -585,7 +634,7 @@ class WikiConfig
        #
        # * key: `wiki.desc`
        # * default: ``
-       fun wiki_desc: String is cached do return value_or_default("wiki.desc", "")
+       var wiki_desc: String is lazy do return value_or_default("wiki.desc", "")
 
        # Site logo url.
        #
@@ -593,14 +642,16 @@ class WikiConfig
        #
        # * key: `wiki.logo`
        # * default: ``
-       fun wiki_logo: String is cached do return value_or_default("wiki.logo", "")
+       var wiki_logo: String is lazy do return value_or_default("wiki.logo", "")
 
-       # Root url of the wiki.
+       # Markdown extension recognized by this wiki.
        #
-       # * key: `wiki.root_url`
-       # * default: `http://localhost/`
-       fun root_url: String is cached do return value_or_default("wiki.root_url", "http://localhost/")
-
+       # We allow only one kind of extension per wiki.
+       # Files with other markdown extensions will be treated as resources.
+       #
+       # * key: `wiki.md_ext`
+       # * default: `md`
+       var md_ext: String is lazy do return value_or_default("wiki.md_ext", "md")
 
        # Root directory of the wiki.
        #
@@ -608,7 +659,7 @@ class WikiConfig
        #
        # * key: `wiki.root_dir`
        # * default: `./`
-       fun root_dir: String is cached do return value_or_default("wiki.root_dir", "./").simplify_path
+       var root_dir: String is lazy do return value_or_default("wiki.root_dir", "./").simplify_path
 
        # Pages directory.
        #
@@ -616,7 +667,7 @@ class WikiConfig
        #
        # * key: `wiki.source_dir
        # * default: `pages/`
-       fun source_dir: String is cached do
+       var source_dir: String is lazy do
                return value_or_default("wiki.source_dir", "pages/").simplify_path
        end
 
@@ -627,7 +678,7 @@ class WikiConfig
        #
        # * key: `wiki.out_dir`
        # * default: `out/`
-       fun out_dir: String is cached do return value_or_default("wiki.out_dir", "out/").simplify_path
+       var out_dir: String is lazy do return value_or_default("wiki.out_dir", "out/").simplify_path
 
        # Asset files directory.
        #
@@ -636,7 +687,7 @@ class WikiConfig
        #
        # * key: `wiki.assets_dir`
        # * default: `assets/`
-       fun assets_dir: String is cached do
+       var assets_dir: String is lazy do
                return value_or_default("wiki.assets_dir", "assets/").simplify_path
        end
 
@@ -647,7 +698,7 @@ class WikiConfig
        #
        # * key: `wiki.templates_dir`
        # * default: `templates/`
-       fun templates_dir: String is cached do
+       var templates_dir: String is lazy do
                return value_or_default("wiki.templates_dir", "templates/").simplify_path
        end
 
@@ -657,7 +708,7 @@ class WikiConfig
        #
        # * key: `wiki.template`
        # * default: `template.html`
-       fun template_file: String is cached do
+       var template_file: String is lazy do
                return value_or_default("wiki.template", "template.html")
        end
 
@@ -668,7 +719,7 @@ class WikiConfig
        #
        # * key: `wiki.header`
        # * default: `header.html`
-       fun header_file: String is cached do
+       var header_file: String is lazy do
                return value_or_default("wiki.header", "header.html")
        end
 
@@ -678,7 +729,7 @@ class WikiConfig
        #
        # * key: `wiki.menu`
        # * default: `menu.html`
-       fun menu_file: String is cached do
+       var menu_file: String is lazy do
                return value_or_default("wiki.menu", "menu.html")
        end
 
@@ -689,29 +740,94 @@ class WikiConfig
        #
        # * key: `wiki.footer`
        # * default: `footer.html`
-       fun footer_file: String is cached do
+       var footer_file: String is lazy do
                return value_or_default("wiki.footer", "footer.html")
        end
 
+       # Automatically add a summary.
+       #
+       # * key: `wiki.auto_summary`
+       # * default: `true`
+       var auto_summary: Bool is lazy do
+               return value_or_default("wiki.auto_summary", "true") == "true"
+       end
+
+       # Automatically add breadcrumbs.
+       #
+       # * key: `wiki.auto_breadcrumbs`
+       # * default: `true`
+       var auto_breadcrumbs: Bool is lazy do
+               return value_or_default("wiki.auto_breadcrumbs", "true") == "true"
+       end
+
+       # Sidebar position.
+       #
+       # Position of the sidebar between `left`, `right` and `none`. Any other value
+       # will be considered as `none`.
+       #
+       # * key: `wiki.sidebar`
+       # * default: `left`
+       var sidebar: String is lazy do
+               return value_or_default("wiki.sidebar", "left")
+       end
+
+       # Sidebar markdown block to include.
+       #
+       # Blocks are specified by their filename without the extension.
+       #
+       # * key: `wiki.sidebar.blocks`
+       # * default: `[]`
+       var sidebar_blocks: Array[String] is lazy do
+               var res = new Array[String]
+               if not has_key("wiki.sidebar.blocks") then return res
+               for val in at("wiki.sidebar.blocks").as(not null).values do
+                       res.add val
+               end
+               return res
+       end
+
+       # Sidebar files directory.
+       #
+       # Directory where sidebar blocks are stored.
+       # **This path MUST be relative to `root_dir`.**
+       #
+       # * key: `wiki.sidebar_dir`
+       # * default: `sidebar/`
+       var sidebar_dir: String is lazy do
+               return value_or_default("wiki.sidebar_dir", "sidebar/").simplify_path
+       end
+
        # Directory used by rsync to upload wiki files.
        #
        # This information is used to update your distant wiki files (like the webserver).
        #
        # * key: `wiki.rsync_dir`
        # * default: ``
-       fun rsync_dir: String is cached do return value_or_default("wiki.rsync_dir", "")
+       var rsync_dir: String is lazy do return value_or_default("wiki.rsync_dir", "")
 
        # Remote repository used to pull modifications on sources.
        #
        # * key: `wiki.git_origin`
        # * default: `origin`
-       fun git_origin: String is cached do return value_or_default("wiki.git_origin", "origin")
+       var git_origin: String is lazy do return value_or_default("wiki.git_origin", "origin")
 
        # Remote branch used to pull modifications on sources.
        #
        # * key: `wiki.git_branch`
        # * default: `master`
-       fun git_branch: String is cached do return value_or_default("wiki.git_branch", "master")
+       var git_branch: String is lazy do return value_or_default("wiki.git_branch", "master")
+
+       # URL to source versionning used to display last changes
+       #
+       # * key: `wiki.last_changes`
+       # * default: ``
+       var last_changes: String is lazy do return value_or_default("wiki.last_changes", "")
+
+       # URL to source edition.
+       #
+       # * key: `wiki.edit`
+       # * default: ``
+       var edit: String is lazy do return value_or_default("wiki.edit", "")
 end
 
 # WikiSection custom configuration.