Introduces `nitiwiki` a contrib tool that generate an html wiki structure based on markdown files.
Behavior is very similar to ikiwiki (but simplified).
Features:
* automatic wiki structure from folders hierarchy
* automatic site menu
* automatic sitemap
* automatic summaries
* easy and rapid templating
* customizable section templates and menus
* rsync synchronization
* git synchronization
See http://moz-code.org/nitiwiki/ for online documentation. This is actually the `README.md` file rendered by nitiwiki. The full wiki can be found in `contrib/examples/nitiwiki`
Another example can be found at http://moz-code.org/uqam/. This is my TA site, entirely based on nitiwiki.
Pull-Request: #765
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>
--- /dev/null
+all: nitiwiki
+
+nitiwiki:
+ mkdir -p bin
+ ../../bin/nitc src/nitiwiki.nit -o bin/nitiwiki
+
+tests: nitiwiki
+ cd tests; make
+
+doc:
+ ../../bin/nitdoc -d doc src/nitiwiki.nit
+
+clean:
+ rm -rf bin
+ rm -rf -- .nit_compile 2> /dev/null || true
--- /dev/null
+# nitiwiki, a simple wiki manager based on markdown.
+
+Basically, nitiwiki compiles a set of markdown files into an HTML wiki.
+
+The wiki content is structured by `sections` represented by the wiki folders. Sections can contain `articles` represented by markdown files.
+
+Features:
+
+* automatic wiki structure from folders hierarchy
+* automatic site menu
+* automatic sitemap
+* automatic summaries
+* easy and rapid templating
+* customizable section templates and menus
+* rsync synchronization
+* git synchronization
+
+## Wiki structure
+
+Basic wiki structure:
+
+ root
+ |- assets
+ |- out
+ |- pages
+ |- templates
+ | |- footer.html
+ | |- header.html
+ | |- menu.html
+ | `- template.html
+ `- config.ini
+
+### pages
+
+This is where goes all the content of your wiki.
+Nitiwiki will render an article for each markdown file found in `pages`.
+
+You can categorize your content in sections using sub-folders:
+
+ pages
+ |- section1
+ | `- index.md
+ |- section2
+ | `- index.md
+ |- page1.md
+ |- page2.md
+ `- index.md
+
+### assets
+
+This is where you store CSS and JavaScript files used in the design of your site.
+
+You can also use this directory to put some images or other files that will be
+used in all your pages.
+
+ assets
+ |- css
+ |- js
+ `- logo.png
+
+### templates
+
+This folder contains the templates used to generate the HTML pages of your wiki.
+
+The main template is called `template.html`.
+It contains the HTML structure of your pages and some macros that will be replaced
+by the wiki articles.
+
+### out
+
+This is where your wiki will be rendered by nitiwiki.
+Do not put anything in this folder since it will be overwritten at the
+next wiki rendering.
+
+The wiki rendering consists in:
+
+1. copy the `assets` directory to `out`
+2. copy attached article files from `pages` to `out`
+3. translate markdown files from `pages` to html files in `out`
+
+### config.ini
+
+This is the main config file of your wiki. For more details see [Configure the wiki](#Configure_the_wiki).
+
+## Manage the wiki
+
+### Create a new wiki
+
+Just move to the directory where you want to store your source files and type:
+
+ nitiwiki init
+
+This command will import the base structure of your wiki in the current directory.
+At this point nitiwiki has created the main configuration file of your site:
+`config.ini`.
+
+### Configure the wiki
+
+All the nitiwiki configuration is done using
+[ini files](http://en.wikipedia.org/wiki/INI_file).
+
+The wiki configuration is contained in the `config.ini` file located at the root
+directory of your wiki.
+This file can be edited to change nitiwiki settings.
+
+Settings:
+
+* `wiki.name`: Displayed name
+* `wiki.desc`: Long description
+* `wiki.logo`: Logo image url
+* `wiki.root_url`: Base url used to resolve links
+* `wiki.root_dir`: Absolute path of base directory
+* `wiki.source_dir`: Source directory (relative path from `wiki.root_dir`)
+* `wiki.out_dir`: Output directory (relative)
+* `wiki.assets_dir`: Assets directory (relative)
+* `wiki.templates_dir`: Templates directory (relative)
+* `wiki.template`: Wiki main template file
+* `wiki.header`: Wiki main header template file
+* `wiki.footer`: Wiki main footer template file
+* `wiki.menu`: Wiki main menu template file
+* `wiki.rsync_dir`: Directory used to rsync output
+* `wiki.git_origin`: Git origin used to fetch data
+* `wiki.git_branch`: Git branch used to fetch data
+
+For more details on each option see `WikiConfig`.
+
+### Add content
+
+To add content in your wiki simply add markdown files (with `.md` extension) into the `pages/` folder.
+
+Once you have done your changes, use:
+
+ nitiwiki --status
+
+This will show the impacts of your changes on the wiki structure.
+
+Then type:
+
+ nitiwiki --render
+
+This will the generate the html output of your new content.
+The option `--force` can be used to regenerate all the wiki.
+This can be uselful when you perform changes on the template files.
+
+### Configure sections
+
+Section appearance can be customized using config files.
+
+Each section in the `pages/` folder can contain a `config.ini` file.
+Options set on a section will be propagated to all its children unless
+they have their own config file.
+
+Allowed options are:
+
+* `section.title`: Custom title for this section
+* `section.template`: Custom template file
+* `section.header`: Custom header template file
+* `section.footer`: Custom footer template file
+* `section.menu`: Custom menu template file
+* `section.is_hidden`: Set this to `true` will hide the section in all menus and
+ sitemaps.
+
+## Customize templates
+
+Templating your wiki involves modifying 4 template files:
+
+* `template.html`
+* `header.html`
+* `footer.html`
+* `menu.html`
+
+Each of these file contains an HTML skeletton used by nitiwiki to render your files.
+Templates can contains macros marked `%MACRO%` that will be replaced by dynamic content.
+
+Every template can access to:
+
+* `ROOT_URL`: Wiki root url
+* `TITLE`: Wiki name
+* `SUBTITLE`: Wiki description
+* `LOGO`: Wiki logo image path
+
+Additionnal macros can be used in specialized templates.
+
+### Main template
+
+The template file `template.html` represents the overall structure of your wiki pages.
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>%TITLE%</title>
+ <link href="%ROOT_URL%/assets/css/main.css" rel="stylesheet">
+ </head>
+ <body>
+ %HEADER%
+ %TOP_MENU%
+ <div>
+ %BODY%
+ %FOOTER%
+ </div>
+ </body>
+ </html>
+
+Additionnal macros:
+
+* `HEADER`: Wiki header (see [Header template](#Header_template))
+* `FOOTER`: Wiki footer (see [Footer template](#Footer_template))
+* `TOP_MENU`: Wiki top menu (see [Topmenu template](#Topmenu_template))
+* `HEADER`: Wiki header (see [Header template](#Header_template))
+* `BODY`: Wiki body content
+
+### Header template
+
+The template file `header.html` is generated on top of all the wiki pages.
+
+ <header>
+ <a href="#"><img src="%ROOT_URL%/%LOGO%" alt="logo"/></a>
+ <h2>%SUBTITLE%</h2>
+ <h1>%TITLE%</h1>
+ </header>
+
+### Footer template
+
+The template file `footer.html` is generated on the bottom of all the wiki pages.
+
+ <footer>
+ <p>%TITLE% © %YEAR%</p>
+ <p>last modification %GEN_TIME%</p>
+ </footer>
+
+Additionnal macros:
+
+* `YEAR`: Current year
+* `GEN_TIME`: Page generation date
+
+### Topmenu template
+
+The template file `menu.html` contains the menu structure generated on all your pages.
+
+Its content can be static:
+
+ <nav class="menu">
+ <ul class="nav navbar-nav">
+ <li><a href="#">Home</a></li>
+ <li><a href="#">Page1</a></li>
+ <li><a href="#">Page2</a></li>
+ </ul>
+ </nav>
+
+Or dynamic using the macro `MENUS`:
+
+ <nav class="menu">
+ <ul class="nav navbar-nav">
+ %MENUS%
+ </ul>
+ </nav>
+
+## Advanced usages
+
+### Working with git
+
+nitiwiki allows you to store your wiki changes in git.
+Using the option `--fetch` will update the local wiki instance
+according to git informations found in the config file.
+
+Be sure to set `wiki.git_origin` and `wiki.git_branch`
+in order to correctly pull changes.
+
+To automatically update your wiki when changes are pushed on the
+origin repository you can use the following command in a git hook:
+
+ nitiwiki --fetch --render
+
+### Working with a remote server
+
+Sometimes you cannot do what you want on your webserver (like setting crons).
+For this purpose, nitiwiki provide a quick way to update a distant instance
+through `ssh` using `rsync`.
+
+With the option `--rsync`, nitwiki will update a distant location with the
+last rendered output. This way you can manually update your webserver
+after changes or set a cron on a different server that you can control.
+
+Using the following command in your cron will update the web server instance
+from git:
+
+ nitiwiki --fetch --render --rsync
+
+Be sure to set `wiki.rsync_dir` in order to correctly push your changes.
+When using `--rsync`, keep in mind that the rendered output must be configured
+to work on the web server and set `wiki.root_url` accordingly.
--- /dev/null
+wiki.name=MyWiki
+wiki.desc=proudly powered by nit
+wiki.logo=assets/logo.png
+wiki.root_url=http://localhost/
+wiki.root_dir=/full/path/to/your/wiki/root/dir
--- /dev/null
+# Welcome on nitiwiki
--- /dev/null
+<footer>
+ <p>%TITLE% © %YEAR%</p>
+ <p>last modification %GEN_TIME%</p>
+</footer>
--- /dev/null
+<header>
+ <a href="#"><img src="%ROOT_URL%/%LOGO%" alt="logo"/></a>
+ <h2>%SUBTITLE%</h2>
+ <h1>%TITLE%</h1>
+</header>
--- /dev/null
+<nav class="menu">
+ <ul class="nav navbar-nav">
+ %MENUS%
+ </ul>
+</nav>
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>%TITLE%</title>
+ <link href="%ROOT_URL%/assets/css/main.css" rel="stylesheet">
+ </head>
+ <body>
+ %HEADER%
+ %TOP_MENU%
+ <div>
+ %BODY%
+ %FOOTER%
+ </div>
+ </body>
+</html>
--- /dev/null
+header { margin: 15px 0; }
+header img {
+ padding: 10px;
+ float: left;
+}
+header h1, header h2 { display: none; }
+
+footer {
+ margin-top: 30px;
+ text-align: center;
+}
+
+.menu {
+ border-top: 6px solid #47CA42;
+ background-color: #0d8921;
+}
+
+.menu .navbar-brand, .menu a { color: #fff; }
+.menu .navbar-nav>.active>a, .menu .navbar-nav>li>a:hover { background-color: #47CA42; }
+
+.sidebar { margin-top: 20px; }
+
+.summary a { color: #333; }
+.summary a:hover { text-decoration: none; }
+.summary a:hover, .summary li:active>a { color: #428bca; }
+.summary li { padding: 2px 0; font-weight: bold }
+.summary li li { font-size: 13px;}
+.summary li li li { font-size: 12px; font-weight: normal }
+
+.breadcrumb { margin-top: 20px; }
--- /dev/null
+wiki.name=nitiwiki
+wiki.desc=proudly powered by nit
+wiki.logo=assets/logo.png
+wiki.root_url=http://moz-code.org/nitiwiki/
+wiki.root_dir=.
+wiki.rsync_dir=moz-code.org:nitiwiki/
--- /dev/null
+../../../README.md
\ No newline at end of file
--- /dev/null
+<footer class="row">
+ <div class="container-fluid">
+ <div class="well well-sm">
+ <p><strong>%TITLE% © %YEAR%</strong></p>
+ <p class="text-muted"><em>last modification %GEN_TIME%</em></p>
+ <p class="text-muted">Proudly powered by
+ <a href="http://nitlanguage.org">nit</a>!</p>
+ </div>
+ </div>
+</footer>
--- /dev/null
+<div class="container-fluid header">
+ <div class="container">
+ <header>
+ <a href="http://uqam.ca"><img src="%ROOT_URL%/%LOGO%" alt="logo" /></a>
+ <h2>%SUBTITLE%</h2>
+ <h1>%TITLE%</h1>
+ </header>
+ </div>
+</div>
--- /dev/null
+<nav class="menu" role="navigation">
+ <div class="container">
+ <!-- Brand and toggle get grouped for better mobile display -->
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" href="%ROOT_URL%index.html">%TITLE%</a>
+ </div>
+ <!-- Collect the nav links, forms, and other content for toggling -->
+ <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
+ <ul class="nav navbar-nav">
+ %MENUS%
+ </ul>
+ </div><!-- /.navbar-collapse -->
+ </div>
+</nav>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>%TITLE%</title>
+
+ <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet">
+ <link href="%ROOT_URL%/assets/css/main.css" rel="stylesheet">
+
+ <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
+ <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
+ <!--[if lt IE 9]>
+ <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
+ <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
+ <![endif]-->
+ </head>
+ <body>
+ %HEADER%
+ %TOP_MENU%
+ <div class="container">
+ <div class="row">
+ %BODY%
+ </div>
+ %FOOTER%
+ </div>
+
+ <script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
+ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
+ </body>
+</html>
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A wiki engine based on markdown files and git.
+module nitiwiki
+
+import wiki_html
+
+# Locate nit directory
+private fun compute_nit_dir(opt_nit_dir: OptionString): String do
+ # the option has precedence
+ var res = opt_nit_dir.value
+ if res != null then
+ if not check_nit_dir(res) then
+ print "Fatal Error: --nit-dir does not seem to be a valid base Nit directory: {res}"
+ exit 1
+ end
+ return res
+ end
+
+ # then the environ variable has precedence
+ res = "NIT_DIR".environ
+ if not res.is_empty then
+ if not check_nit_dir(res) then
+ print "Fatal Error: NIT_DIR does not seem to be a valid base Nit directory: {res}"
+ exit 1
+ end
+ return res
+ end
+
+ # find the runpath of the program from argv[0]
+ res = "{sys.program_name.dirname}/.."
+ if check_nit_dir(res) then return res.simplify_path
+
+ # find the runpath of the process from /proc
+ var exe = "/proc/self/exe"
+ if exe.file_exists then
+ res = exe.realpath
+ res = res.dirname.join_path("..")
+ if check_nit_dir(res) then return res.simplify_path
+ end
+
+ # search in the PATH
+ var ps = "PATH".environ.split(":")
+ for p in ps do
+ res = p/".."
+ if check_nit_dir(res) then return res.simplify_path
+ end
+
+ print "Fatal Error: Cannot locate a valid base nit directory. It is quite unexpected. Try to set the environment variable `NIT_DIR` or to use the `--nit-dir` option."
+ exit 1
+ abort
+end
+
+private fun check_nit_dir(res: String): Bool do
+ return res.file_exists and "{res}/src/nit.nit".file_exists
+end
+
+var opt_help = new OptionBool("Display this help message", "-h", "--help")
+var opt_verbose = new OptionCount("Verbose level", "-v")
+var opt_config = new OptionString("Path to config.ini file", "-c", "--config")
+var opt_init = new OptionBool("Initialize a new wiki in the current directory", "--init")
+var opt_status = new OptionBool("Display wiki status", "-s", "--status")
+var opt_render = new OptionBool("Render the out directory from markdown sources", "-r", "--render")
+var opt_force = new OptionBool("Force render even if source files are unchanged", "-f", "--force")
+var opt_clean = new OptionBool("Clean the output directory", "--clean")
+var opt_rsync = new OptionBool("Synchronize outdir with distant wiki using rsync", "-s", "--rsync")
+var opt_fetch = new OptionBool("Render local source from git repo", "--fetch")
+var opt_nit_dir = new OptionString("Nit base directory", "--nit-dir")
+
+var context = new OptionContext
+context.add_option(opt_help, opt_verbose, opt_config)
+context.add_option(opt_init, opt_status, opt_render, opt_force)
+context.add_option(opt_clean, opt_rsync, opt_fetch, opt_nit_dir)
+context.parse(args)
+
+var config_filename = "config.ini"
+
+# --help
+if opt_help.value then
+ context.usage
+ exit 0
+end
+
+# --init
+if opt_init.value then
+ if config_filename.file_exists then
+ print "Already in a nitiwiki directory."
+ exit 0
+ end
+ var nitiwiki_home = "{compute_nit_dir(opt_nit_dir)}/contrib/nitiwiki"
+ var tpl = "{nitiwiki_home}/examples/default/"
+ if not tpl.file_exists then
+ print "Cannot find {tpl} files."
+ print "Maybe your NIT_DIR is not set properly?"
+ print "You can initialize nitiwiki manually by copying the default skeletton here."
+ exit 1
+ end
+ sys.system "cp -R {tpl}/* ."
+ print "Initialized new nitiwiki."
+ print "Set wiki settings by editing {config_filename}."
+ exit 0
+end
+
+# load config files
+
+# --config
+var config_file = opt_config.value
+if config_file == null then
+ config_file = config_filename
+end
+
+if not config_file.file_exists then
+ print "Not in a nitiwiki directory."
+ print "Use --init to initialize one here."
+ exit 0
+end
+
+var config = new WikiConfig(config_file)
+var wiki = new Nitiwiki(config)
+
+# --verbose
+wiki.verbose_level = opt_verbose.value
+
+# --clean
+if opt_clean.value then
+ wiki.clean
+end
+
+# --fetch
+if opt_fetch.value then
+ wiki.fetch
+end
+
+# --render
+if opt_render.value then
+ wiki.parse
+ # --force
+ wiki.force_render = opt_force.value
+ wiki.render
+end
+
+# --rsync
+if opt_rsync.value then
+ wiki.sync
+end
+
+# --status
+if opt_status.value or args.is_empty then
+ wiki.parse
+ wiki.status
+end
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Base entities of a nitiwiki.
+module wiki_base
+
+import template::macro
+import markdown
+import opts
+import ini
+
+# A Nitiwiki instance.
+#
+# Nitiwiki provide all base services used by `WikiSection` and `WikiArticle`.
+# It manages content and renders pages.
+#
+# Each nitiwiki instance is linked to a config file.
+# This file show to `nitiwiki` that a wiki is present in the current directory.
+# Without it, nitiwiki will consider the directory as empty.
+class Nitiwiki
+
+ # Wiki config object.
+ var config: WikiConfig
+
+ # Default config filename.
+ var config_filename = "config.ini"
+
+ # Force render on all file even if the source is unmodified.
+ var force_render = false is writable
+
+ # Verbosity level.
+ var verbose_level = 0 is writable
+
+ # Delete all the output files.
+ fun clean do
+ var out_dir = expand_path(config.root_dir, config.out_dir)
+ if out_dir.file_exists then out_dir.rmdir
+ end
+
+ # 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}"
+ end
+
+ fun fetch do
+ sys.system "git pull {config.git_origin} {config.git_branch}"
+ end
+
+ # Analyze wiki files from `dir` to build wiki entries.
+ #
+ # This method build a hierarchical structure of `WikiSection` and `WikiArticle`
+ # based on the markdown source structure.
+ fun parse do
+ var dir = expand_path(config.root_dir, config.source_dir)
+ root_section = new_section(dir)
+ var files = list_md_files(dir)
+ for file in files do
+ new_article(file)
+ end
+ 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:"
+ var paths = entries.keys.to_a
+ var s = new DefaultComparator
+ s.sort(paths)
+ for path in paths do
+ 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.is_new then
+ print " + {name}"
+ else
+ print " * {name}"
+ end
+ end
+ print ""
+ print "Use nitiwiki --render to render modified files"
+ else
+ print "Wiki is up-to-date"
+ print ""
+ print "Use nitiwiki --fetch to pull modification from origin"
+ print "Use nitiwiki --rsync to synchronize distant output"
+ end
+ end
+
+ # Display msg if `level >= verbose_level`
+ fun message(msg: String, level: Int) do
+ if level <= verbose_level then print msg
+ end
+
+ # 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")
+ while not pipe.eof do
+ var file = pipe.read_line
+ if file == "" then break # last line
+ file = file.substring(0, file.length - 1) # strip last oef
+ var name = file.basename(".md")
+ if name == "header" or name == "footer" or name == "menu" then continue
+ files.add file
+ end
+ pipe.close
+ pipe.wait
+ if pipe.status != 0 then exit 1
+ var s = new DefaultComparator
+ s.sort(files)
+ return files
+ end
+
+ # Does `src` have been modified since `target` creation?
+ #
+ # Always returns `true` if `--force` is on.
+ 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
+ end
+
+ # Create a new `WikiSection`.
+ #
+ # `path` is used to determine the place in the wiki hierarchy.
+ protected fun new_section(path: String): WikiSection do
+ 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 section = new WikiSection(self, name)
+ entries[path] = section
+ if path == root then return section
+ var ppath = path.dirname
+ if ppath != path then
+ var parent = new_section(ppath)
+ parent.add_child(section)
+ end
+ section.try_load_config
+ return section
+ end
+
+ # Create a new `WikiArticle`.
+ #
+ # `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)
+ var article = new WikiArticle.from_source(self, path)
+ var section = new_section(path.dirname)
+ section.add_child(article)
+ entries[path] = article
+ return article
+ end
+
+ # Wiki entries found in the last `lookup_hierarchy`.
+ var entries = new HashMap[String, WikiEntry]
+
+ # The root `WikiSection` of the site found in the last `lookup_hierarchy`.
+ var root_section: WikiSection is noinit
+
+ # Does a template named `name` exists for this wiki?
+ fun has_template(name: String): Bool do
+ return expand_path(config.root_dir, config.templates_dir, name).file_exists
+ end
+
+ # Load a template file as a `TemplateString`.
+ #
+ # REQUIRE: `has_template`
+ fun load_template(name: String): TemplateString do
+ assert has_template(name)
+ 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
+ if tpl.has_macro("SUBTITLE") then
+ tpl.replace("SUBTITLE", config.wiki_desc)
+ end
+ if tpl.has_macro("LOGO") then
+ tpl.replace("LOGO", config.wiki_logo)
+ end
+ return tpl
+ end
+
+ # Join `parts` as a path and simplify it
+ fun expand_path(parts: String...): String do
+ var path = ""
+ for part in parts do
+ path = path.join_path(part)
+ end
+ return path.simplify_path
+ end
+
+ fun pretty_name(name: String): String do
+ name = name.replace("_", " ")
+ name = name.capitalized
+ return name
+ end
+end
+
+# A wiki is composed of hierarchical entries.
+abstract class WikiEntry
+
+ # `Nitiwiki` this entry belongs to.
+ var wiki: Nitiwiki
+
+ # Entry data
+
+ # Entry internal name.
+ #
+ # Mainly used in urls.
+ var name: String
+
+ # Displayed title for `self`.
+ #
+ # If `self` is the root entry then display the wiki `WikiConfig::wiki_name` instead.
+ fun title: String do
+ if is_root then return wiki.config.wiki_name
+ return wiki.pretty_name(name)
+ end
+
+ # Is this section rendered from a source document?
+ #
+ # Source is an abstract concept at this level.
+ # It can represent a directory, a source file,
+ # a part of a file, everything needed to
+ # extend this base framework.
+ fun has_source: Bool do return src_path != null
+
+ # Entry creation time.
+ #
+ # 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
+ end
+
+ # Entry last modification time.
+ #
+ # 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
+ end
+
+ # Entry list rendering time.
+ #
+ # Returns `-1` if `is_new`.
+ fun last_render_time: Int do
+ if is_new then return -1
+ return out_full_path.file_stat.mtime
+ end
+
+ # Entries hierarchy
+
+ # Type of the parent entry.
+ type PARENT: WikiEntry
+
+ # Parent entry if any.
+ var parent: nullable PARENT = null
+
+ # Does `self` have a parent?
+ fun is_root: Bool do return parent == null
+
+ # Children labelled by `name`.
+ var children = new HashMap[String, WikiEntry]
+
+ # Does `self` have a child nammed `name`?
+ fun has_child(name: String): Bool do return children.keys.has(name)
+
+ # Retrieve the child called `name`.
+ fun child(name: String): WikiEntry do return children[name]
+
+ # Add a sub-entry to `self`.
+ fun add_child(entry: WikiEntry) do
+ entry.parent = self
+ children[entry.name] = entry
+ end
+
+ # Paths and urls
+
+ # Breadcrumbs from the `Nitiwiki::root_section` to `self`.
+ #
+ # 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 path = new Array[WikiEntry]
+ var entry: nullable WikiEntry = self
+ while entry != null and not entry.is_root do
+ path.add entry
+ entry = entry.parent
+ end
+ return path.reversed
+ end
+
+ # Relative path from `wiki.config.root_dir` to source if any.
+ fun src_path: nullable String is abstract
+
+ # Absolute path to the source if any.
+ fun src_full_path: nullable String do
+ var src = src_path
+ if src == null then return null
+ return wiki.config.root_dir.join_path(src)
+ end
+
+ # Relative path from `wiki.config.root_dir` to rendered output.
+ #
+ # Like `src_path`, this method can represent a
+ # directory or a file.
+ fun out_path: String is abstract
+
+ # Absolute path to the output.
+ fun out_full_path: String do return wiki.config.root_dir.join_path(out_path)
+
+ # Rendering
+
+ # Does `self` have already been rendered?
+ fun is_new: Bool do return not out_full_path.file_exists
+
+ # Does `self` rendered output is outdated?
+ #
+ # Returns `true` if `is_new` then check in children.
+ fun is_dirty: Bool do
+ if is_new then return true
+ if has_source then
+ if last_edit_time >= last_render_time then return true
+ end
+ for child in children.values do
+ if child.is_dirty then return true
+ end
+ return false
+ end
+
+ # Render `self` and `children` is needed.
+ fun render do for child in children.values do child.render
+
+ # Templating
+
+ # Template file for `self`.
+ #
+ # Each entity can use a custom template.
+ # By default the template is inherited from the parent.
+ #
+ # If the root does not have a custom template,
+ # 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
+ end
+
+ # Header template file for `self`.
+ #
+ # Behave like `template_file`.
+ fun header_file: String do
+ if is_root then return wiki.config.header_file
+ return parent.header_file
+ end
+
+ # Footer template file for `self`.
+ #
+ # Behave like `template_file`.
+ fun footer_file: String do
+ if is_root then return wiki.config.footer_file
+ return parent.footer_file
+ end
+
+ # Menu template file for `self`.
+ #
+ # Behave like `template_file`.
+ fun menu_file: String do
+ if is_root then return wiki.config.menu_file
+ return parent.menu_file
+ end
+
+ # Display the entry `name`.
+ redef fun to_s do return name
+end
+
+# Each WikiSection is related to a source directory.
+#
+# A section can contain other sub-sections or pages.
+class WikiSection
+ super WikiEntry
+
+ # A section can only have another section as parent.
+ redef type PARENT: WikiSection
+
+ redef fun title do
+ if has_config then
+ var title = config.title
+ if title != null then return title
+ end
+ return super
+ end
+
+ # Is this section hidden?
+ #
+ # Hidden section are rendered but not linked in menus.
+ fun is_hidden: Bool do
+ if has_config then return config.is_hidden
+ return false
+ end
+
+ # Source directory.
+ redef fun src_path: String do
+ if parent == null then
+ return wiki.config.source_dir
+ else
+ return wiki.expand_path(parent.src_path, name)
+ end
+ end
+
+ # Config
+
+ # Custom configuration file for this section.
+ var config: nullable SectionConfig = null
+
+ # Does this section have its own config file?
+ fun has_config: Bool do return config != null
+
+ # Try to load the config file for this section.
+ private fun try_load_config do
+ var cfile = wiki.expand_path(wiki.config.root_dir, src_path, wiki.config_filename)
+ if not cfile.file_exists then return
+ wiki.message("Custom config for section {name}", 2)
+ config = new SectionConfig(cfile)
+ end
+
+ # Templating
+
+ # Also check custom config.
+ redef fun template_file do
+ if has_config then
+ var tpl = config.template_file
+ if tpl != null then return tpl
+ end
+ if is_root then return wiki.config.template_file
+ return parent.template_file
+ end
+
+ # Also check custom config.
+ redef fun header_file do
+ if has_config then
+ var tpl = config.header_file
+ if tpl != null then return tpl
+ end
+ if is_root then return wiki.config.header_file
+ return parent.header_file
+ end
+
+ # Also check custom config.
+ redef fun footer_file do
+ if has_config then
+ var tpl = config.footer_file
+ if tpl != null then return tpl
+ end
+ if is_root then return wiki.config.footer_file
+ return parent.footer_file
+ end
+
+ # Also check custom config.
+ redef fun menu_file do
+ if has_config then
+ var tpl = config.menu_file
+ if tpl != null then return tpl
+ end
+ if is_root then return wiki.config.menu_file
+ return parent.menu_file
+ end
+end
+
+# Each WikiArticle is related to a HTML file.
+#
+# Article can be created from scratch using this API or
+# automatically from a markdown source file (see: `from_source`).
+class WikiArticle
+ super WikiEntry
+
+ # Articles can only have `WikiSection` as parents.
+ redef type PARENT: WikiSection
+
+ redef fun title: String do
+ if name == "index" and parent != null then return parent.title
+ return super
+ end
+
+ # 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]
+
+ # Create a new articleu sing 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
+ end
+
+ redef var src_full_path: nullable String = null
+
+ redef fun src_path do
+ if src_full_path == null then return null
+ return src_full_path.substring_from(wiki.config.root_dir.length)
+ end
+
+ # The page markdown source content.
+ #
+ # 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 = file.read_all
+ file.close
+ return md
+ end
+
+ # Returns true if has source and
+ # `last_edit_date` > 'last_render_date'.
+ 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)
+ end
+ return false
+ end
+
+ redef fun to_s do return "{name} ({parent or else "null"})"
+end
+
+# Wiki configuration class.
+#
+# This class provides services that ensure static typing when accessing the `config.ini` file.
+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]
+ end
+
+ # Site name displayed.
+ #
+ # The title is used as home title and in headers.
+ #
+ # * key: `wiki.name`
+ # * default: `MyWiki`
+ fun wiki_name: String is cached do return value_or_default("wiki.name", "MyWiki")
+
+ # Site description.
+ #
+ # Displayed in header.
+ #
+ # * key: `wiki.desc`
+ # * default: ``
+ fun wiki_desc: String is cached do return value_or_default("wiki.desc", "")
+
+ # Site logo url.
+ #
+ # Url of the image to be displayed in header.
+ #
+ # * key: `wiki.logo`
+ # * default: ``
+ fun wiki_logo: String is cached do return value_or_default("wiki.logo", "")
+
+ # Root url of the wiki.
+ #
+ # * key: `wiki.root_url`
+ # * default: `http://localhost/`
+ fun root_url: String is cached do return value_or_default("wiki.root_url", "http://localhost/")
+
+
+ # Root directory of the wiki.
+ #
+ # Directory where the wiki files are stored locally.
+ #
+ # * key: `wiki.root_dir`
+ # * default: `./`
+ fun root_dir: String is cached do return value_or_default("wiki.root_dir", "./").simplify_path
+
+ # Pages directory.
+ #
+ # Directory where markdown source files are stored.
+ #
+ # * key: `wiki.source_dir
+ # * default: `pages/`
+ fun source_dir: String is cached do
+ return value_or_default("wiki.source_dir", "pages/").simplify_path
+ end
+
+ # Output directory.
+ #
+ # Directory where public wiki files are generated.
+ # **This path MUST be relative to `root_dir`.**
+ #
+ # * key: `wiki.out_dir`
+ # * default: `out/`
+ fun out_dir: String is cached do return value_or_default("wiki.out_dir", "out/").simplify_path
+
+ # Asset files directory.
+ #
+ # Directory where public assets like JS scripts or CSS files are stored.
+ # **This path MUST be relative to `root_dir`.**
+ #
+ # * key: `wiki.assets_dir`
+ # * default: `assets/`
+ fun assets_dir: String is cached do
+ return value_or_default("wiki.assets_dir", "assets/").simplify_path
+ end
+
+ # Template files directory.
+ #
+ # Directory where template used in HTML generation are stored.
+ # **This path MUST be relative to `root_dir`.**
+ #
+ # * key: `wiki.templates_dir`
+ # * default: `templates/`
+ fun templates_dir: String is cached do
+ return value_or_default("wiki.templates_dir", "templates/").simplify_path
+ end
+
+ # Main template file.
+ #
+ # The main template is used to specify the overall structure of a page.
+ #
+ # * key: `wiki.template`
+ # * default: `template.html`
+ fun template_file: String is cached do
+ return value_or_default("wiki.template", "template.html")
+ end
+
+ # Main header template file.
+ #
+ # Used to specify the structure of the page header.
+ # This is generally the place where you want to put your logo and wiki title.
+ #
+ # * key: `wiki.header`
+ # * default: `header.html`
+ fun header_file: String is cached do
+ return value_or_default("wiki.header", "header.html")
+ end
+
+ # Main menu template file.
+ #
+ # Used to specify the menu structure.
+ #
+ # * key: `wiki.menu`
+ # * default: `menu.html`
+ fun menu_file: String is cached do
+ return value_or_default("wiki.menu", "menu.html")
+ end
+
+ # Main footer file.
+ #
+ # The main footer is used to specify the structure of the page footer.
+ # This is generally the place where you want to put your copyright.
+ #
+ # * key: `wiki.footer`
+ # * default: `footer.html`
+ fun footer_file: String is cached do
+ return value_or_default("wiki.footer", "footer.html")
+ 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", "")
+
+ # 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")
+
+ # 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")
+end
+
+# WikiSection custom configuration.
+#
+# Each section can provide its own config file to customize
+# appearance or behavior.
+class SectionConfig
+ super ConfigTree
+
+ # Returns the config value at `key` or `null` if no key was found.
+ private fun value_or_null(key: String): nullable String do
+ if not has_key(key) then return null
+ return self[key]
+ end
+
+ # Is this section hidden in sitemap and trees and menus?
+ fun is_hidden: Bool do return value_or_null("section.hidden") == "true"
+
+ # Custom section title if any.
+ fun title: nullable String do return value_or_null("section.title")
+
+ # Custom template file if any.
+ fun template_file: nullable String do return value_or_null("section.template")
+
+ # Custom header file if any.
+ fun header_file: nullable String do return value_or_null("section.header")
+
+ # Custom menu file if any.
+ fun menu_file: nullable String do return value_or_null("section.menu")
+
+ # Custom footer file if any.
+ fun footer_file: nullable String do return value_or_null("section.footer")
+end
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# HTML wiki rendering
+module wiki_html
+
+import wiki_base
+
+redef class Nitiwiki
+
+ # Render HTML output looking for changes in the markdown sources.
+ fun render do
+ if not root_section.is_dirty and not force_render then return
+ var out_dir = expand_path(config.root_dir, config.out_dir)
+ out_dir.mkdir
+ copy_assets
+ root_section.add_child make_sitemap
+ root_section.render
+ end
+
+ # Copy the asset directory to the (public) output directory.
+ private fun copy_assets do
+ var src = expand_path(config.root_dir, config.assets_dir)
+ var out = expand_path(config.root_dir, config.out_dir)
+ if need_render(src, expand_path(out, config.assets_dir)) then
+ if src.file_exists then sys.system "cp -R {src} {out}"
+ end
+ end
+
+ # Build the wiki sitemap page.
+ private fun make_sitemap: WikiSitemap do
+ var sitemap = new WikiSitemap(self, "sitemap")
+ sitemap.is_dirty = true
+ return sitemap
+ end
+end
+
+redef class WikiEntry
+
+ # Url to `self` once generated.
+ fun url: String do return wiki.config.root_url.join_path(breadcrumbs.join("/"))
+
+ # Get a `<a>` template link to `self`
+ fun tpl_link: Streamable do
+ return "<a href=\"{url}\">{title}</a>"
+ end
+end
+
+redef class WikiSection
+
+ # Output directory (where to ouput the HTML pages for this section).
+ redef fun out_path: String do
+ if parent == null then
+ return wiki.config.out_dir
+ else
+ return wiki.expand_path(parent.out_path, name)
+ end
+ end
+
+ redef fun render do
+ if not is_dirty and not wiki.force_render then return
+ if is_new then
+ out_full_path.mkdir
+ else
+ sys.system "touch {out_full_path}"
+ end
+ if has_source then
+ wiki.message("Render section {out_path}", 1)
+ copy_files
+ end
+ var index = self.index
+ if index isa WikiSectionIndex then
+ index.is_dirty = true
+ add_child index
+ end
+ super
+ end
+
+ # Copy attached files from `src_path` to `out_path`.
+ private fun copy_files do
+ assert has_source
+ var dir = src_full_path.to_s
+ for name in dir.files do
+ if name == wiki.config_filename then continue
+ if name.has_suffix(".md") then continue
+ if has_child(name) then continue
+ var src = wiki.expand_path(dir, name)
+ var out = wiki.expand_path(out_full_path, name)
+ if not wiki.need_render(src, out) then continue
+ sys.system "cp -R {src} {out_full_path}"
+ end
+ end
+
+ # The index page for this section.
+ #
+ # If no file `index.md` exists for this section,
+ # a summary is generated using contained articles.
+ fun index: WikiArticle is cached do
+ for child in children.values do
+ if child isa WikiArticle and child.is_index then return child
+ end
+ return new WikiSectionIndex(wiki, self)
+ end
+
+ redef fun tpl_link do return index.tpl_link
+
+ # Render the section hierarchy as a html tree.
+ #
+ # `limit` is used to specify the max-depth of the tree.
+ #
+ # The generated tree will be something like this:
+ #
+ # <ul>
+ # <li>section 1</li>
+ # <li>section 2
+ # <ul>
+ # <li>section 2.1</li>
+ # <li>section 2.2</li>
+ # </ul>
+ # </li>
+ # </ul>
+ fun tpl_tree(limit: Int): Template do
+ return tpl_tree_intern(limit, 1)
+ end
+
+ # Build the template tree for this section recursively.
+ protected fun tpl_tree_intern(limit, count: Int): Template do
+ var out = new Template
+ var index = index
+ out.add "<li>"
+ out.add tpl_link
+ if (limit < 0 or count < limit) and
+ (children.length > 1 or (children.length == 1)) then
+ out.add " <ul>"
+ for child in children.values do
+ if child == index then continue
+ if child isa WikiArticle then
+ out.add "<li>"
+ out.add child.tpl_link
+ out.add "</li>"
+ else if child isa WikiSection and not child.is_hidden then
+ out.add child.tpl_tree_intern(limit, count + 1)
+ end
+ end
+ out.add " </ul>"
+ end
+ out.add "</li>"
+ return out
+ end
+end
+
+redef class WikiArticle
+
+ redef fun out_path: String do
+ if parent == null then
+ return wiki.expand_path(wiki.config.out_dir, "{name}.html")
+ else
+ return wiki.expand_path(parent.out_path, "{name}.html")
+ end
+ end
+
+ redef fun url do
+ if parent == null then
+ return wiki.config.root_url.join_path("{name}.html")
+ else
+ return parent.url.join_path("{name}.html")
+ end
+ end
+
+ # Is `self` an index page?
+ #
+ # Checks if `self.name == "index"`.
+ fun is_index: Bool do return name == "index"
+
+ redef fun render do
+ if not is_dirty and not wiki.force_render then return
+ wiki.message("Render article {name}", 2)
+ var file = out_full_path
+ file.dirname.mkdir
+ tpl_page.write_to_file file
+ super
+ end
+
+
+ # Replace macros in the template by wiki data.
+ private fun tpl_page: TemplateString do
+ var tpl = wiki.load_template(template_file)
+ if tpl.has_macro("TOP_MENU") then
+ tpl.replace("TOP_MENU", tpl_menu)
+ end
+ if tpl.has_macro("HEADER") then
+ tpl.replace("HEADER", tpl_header)
+ end
+ if tpl.has_macro("BODY") then
+ tpl.replace("BODY", tpl_article)
+ end
+ if tpl.has_macro("FOOTER") then
+ tpl.replace("FOOTER", tpl_footer)
+ end
+ return tpl
+ end
+
+ # Generate the HTML header for this article.
+ fun tpl_header: Streamable do
+ var file = header_file
+ if not wiki.has_template(file) then return ""
+ return wiki.load_template(file)
+ end
+
+ # Generate the HTML page for this article.
+ fun tpl_article: TplArticle do
+ var article = new TplArticle
+ article.body = content
+ article.breadcrumbs = new TplBreadcrumbs(self)
+ tpl_sidebar.blocks.add tpl_summary
+ article.sidebar = tpl_sidebar
+ return article
+ end
+
+ # Sidebar for this page.
+ var tpl_sidebar = new TplSidebar
+
+ # Generate the HTML summary for this article.
+ #
+ # Based on `headlines`
+ fun tpl_summary: Streamable do
+ var headlines = self.headlines
+ var tpl = new Template
+ tpl.add "<ul class=\"summary list-unstyled\">"
+ var iter = headlines.iterator
+ while iter.is_ok do
+ var hl = iter.item
+ # parse title as markdown
+ var title = hl.title.md_to_html.to_s
+ title = title.substring(3, title.length - 8)
+ tpl.add "<li><a href=\"#{hl.id}\">{title}</a>"
+ iter.next
+ if iter.is_ok then
+ if iter.item.level > hl.level then
+ tpl.add "<ul class=\"list-unstyled\">"
+ else if iter.item.level < hl.level then
+ tpl.add "</li>"
+ tpl.add "</ul>"
+ end
+ else
+ tpl.add "</li>"
+ end
+ end
+ tpl.add "</ul>"
+ return tpl
+ end
+
+ # Generate the HTML menu for this article.
+ fun tpl_menu: Streamable do
+ var file = menu_file
+ if not wiki.has_template(file) then return ""
+ var tpl = wiki.load_template(file)
+ if tpl.has_macro("MENUS") then
+ var items = new Template
+ for child in wiki.root_section.children.values do
+ if child isa WikiArticle and child.is_index then continue
+ if child isa WikiSection and child.is_hidden then continue
+ items.add "<li"
+ if self == child or self.breadcrumbs.has(child) then
+ items.add " class=\"active\""
+ end
+ items.add ">"
+ items.add child.tpl_link
+ items.add "</li>"
+ end
+ tpl.replace("MENUS", items)
+ end
+ return tpl
+ end
+
+ # Generate the HTML footer for this article.
+ fun tpl_footer: Streamable do
+ var file = footer_file
+ if not wiki.has_template(file) then return ""
+ var tpl = wiki.load_template(file)
+ var time = new Tm.gmtime
+ if tpl.has_macro("YEAR") then
+ tpl.replace("YEAR", (time.year + 1900).to_s)
+ end
+ if tpl.has_macro("GEN_TIME") then
+ tpl.replace("GEN_TIME", time.to_s)
+ end
+ return tpl
+ end
+end
+
+# A `WikiArticle` that contains the sitemap tree.
+class WikiSitemap
+ super WikiArticle
+
+ redef fun tpl_article do
+ var article = new TplArticle.with_title("Sitemap")
+ article.body = new TplPageTree(wiki.root_section, -1)
+ return article
+ end
+
+ redef var is_dirty = false
+end
+
+# A `WikiArticle` that contains the section index tree.
+class WikiSectionIndex
+ super WikiArticle
+
+ # The section described by `self`.
+ var section: WikiSection
+
+ init(wiki: Nitiwiki, section: WikiSection) do
+ super(wiki, "index")
+ self.section = section
+ end
+
+ redef var is_dirty = false
+
+ redef fun tpl_article do
+ var article = new TplArticle.with_title(section.title)
+ article.body = new TplPageTree(section, -1)
+ article.breadcrumbs = new TplBreadcrumbs(self)
+ return article
+ end
+end
+
+# Article HTML output.
+class TplArticle
+ super Template
+
+ var title: nullable Streamable = null
+ var body: nullable Streamable = null
+ var sidebar: nullable TplSidebar = null
+ var breadcrumbs: nullable TplBreadcrumbs = null
+
+ init with_title(title: Streamable) do
+ self.title = title
+ end
+
+ redef fun rendering do
+ if sidebar != null then
+ add "<div class=\"col-sm-3 sidebar\">"
+ add sidebar.as(not null)
+ add "</div>"
+ add "<div class=\"col-sm-9 content\">"
+ else
+ add "<div class=\"col-sm-12 content\">"
+ end
+ if body != null then
+ add "<article>"
+ if breadcrumbs != null then
+ add breadcrumbs.as(not null)
+ end
+ if title != null then
+ add "<h1>"
+ add title.as(not null)
+ add "</h1>"
+ end
+ add body.as(not null)
+ add " </article>"
+ end
+ add "</div>"
+ end
+end
+
+# A collection of HTML blocks displayed on the side of a page.
+class TplSidebar
+ super Template
+
+ # Blocks are `Stremable` pieces that will be rendered in the sidebar.
+ var blocks = new Array[Streamable]
+
+ redef fun rendering do
+ for block in blocks do
+ add "<div class=\"sideblock\">"
+ add block
+ add "</div>"
+ end
+ end
+end
+
+# An HTML breadcrumbs that show the path from a `WikiArticle` to the `Nitiwiki` root.
+class TplBreadcrumbs
+ super Template
+
+ # Bread crumb article.
+ var article: WikiArticle
+
+ redef fun rendering do
+ var path = article.breadcrumbs
+ if path.is_empty or path.length <= 2 and article.is_index then return
+ add "<ol class=\"breadcrumb\">"
+ for entry in path do
+ if entry == path.last then
+ add "<li class=\"active\">"
+ add entry.title
+ add "</li>"
+ else
+ if article.parent == entry and article.is_index then continue
+ add "<li>"
+ add entry.tpl_link
+ add "</li>"
+ end
+ end
+ add "</ol>"
+ end
+end
+
+# An HTML tree that show the section pages structure.
+class TplPageTree
+ super Template
+
+ # Builds the page tree from `root`.
+ var root: WikiSection
+
+ # Limits the tree depth to `max_depth` levels.
+ var max_depth: Int
+
+ redef fun rendering do
+ add "<ul>"
+ add root.tpl_tree(-1)
+ add "</ul>"
+ end
+end
--- /dev/null
+# Copyright 2013 Alexandre Terrasa <alexandre@moz-code.org>.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+all: tests
+
+tests: clean
+ ./tests.sh
+
+clean:
+ rm -rf out/
--- /dev/null
+nitiwiki --config wiki1/config.ini --clean --render -v
--- /dev/null
+nitiwiki --config wiki1/config.ini --clean --status
--- /dev/null
+Not in a nitiwiki directory.
+Use --init to initialize one here.
--- /dev/null
+Render section out
--- /dev/null
+nitiWiki
+name: wiki1
+config: wiki1/config.ini
+url: http://localhost/
+
+There is modified files:
+ + pages
+ + /pages/index.md
+
+Use nitiwiki --render to render modified files
--- /dev/null
+#!/bin/bash
+
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Alexandre Terrasa <alexandre@moz-code.org>.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+BIN=../bin
+OUT=./out
+RES=./res
+
+# check args files
+test_args()
+{
+ local test="$1"
+ local args=`cat $test.args`
+ local outdir=$OUT/$test.files
+
+ echo $BIN/$args > $OUT/$test.bin
+ chmod +x $OUT/$test.bin
+ OUTDIR=$outdir $OUT/$test.bin > $OUT/$test.res 2> $OUT/$test.err
+
+ if [ -r $outdir ]; then
+ ls -aR $outdir >> $OUT/$test.res
+ fi
+
+ diff $OUT/$test.res $RES/$test.res > $OUT/$test.diff 2> /dev/null
+}
+
+# return
+# 0 if the sav not exists
+# 1 if the file does match
+# 2 if the file does not match
+check_result() {
+ local test="$1"
+
+ if [ ! -r "$RES/$test.res" ]; then
+ return 0
+ elif [ ! -s $OUT/$test.diff ]; then
+ return 1
+ else
+ return 2
+ fi
+}
+
+echo "Testing..."
+echo ""
+
+rm -rf $OUT 2>/dev/null
+mkdir $OUT 2>/dev/null
+
+all=0
+ok=0
+ko=0
+sk=0
+
+for file in `ls *.args`; do
+ ((all++))
+ test="${file%.*}"
+ echo -n "* $test: "
+
+ test_args $test
+ check_result $test
+
+ case "$?" in
+ 0)
+ echo "skip ($test.res not found)"
+ ((sk++))
+ continue;;
+ 1)
+ echo "success"
+ ((ok++))
+ ;;
+ 2)
+ echo "error (diff $OUT/$test.res $RES/$test.res)"
+ ((ko++))
+ ;;
+ esac
+done
+echo ""
+echo "==> success $ok/$all ($ko tests failed, $sk skipped)"
--- /dev/null
+dummy { margin: 0 }
--- /dev/null
+wiki.name=wiki1
+wiki.root_dir=wiki1/
--- /dev/null
+wiki.name=wiki2
+wiki.desc=the one used by nit/tests.sh
+wiki.root_dir=../contrib/nitiwiki/tests/wiki1/
--- /dev/null
+# Hello World!
--- /dev/null
+<div class="row footer">
+ <div class="container-fluid">
+ <div class="well well-sm">
+ <p><strong>%TITLE% © %YEAR%</strong></p>
+ <p class="text-muted"><em>last modification %GEN_TIME%</em></p>
+ <p class="text-muted">Proudly powered by
+ <a href="http://nitlanguage.org">nit</a>!</p>
+ </div>
+ </div>
+</div>
--- /dev/null
+<div class="container-fluid header">
+ <div class="container">
+ <div class="header">
+ <a href="http://uqam.ca"><img src="%ROOT_URL%/%LOGO%" alt="logo" /></a>
+ <h2>%SUBTITLE%</h2>
+ <h1>%TITLE%</h1>
+ </div>
+ </div>
+</div>
--- /dev/null
+<nav class="menu" role="navigation">
+ <div class="container">
+ <!-- Brand and toggle get grouped for better mobile display -->
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" href="%ROOT_URL%index.html">%TITLE%</a>
+ </div>
+ <!-- Collect the nav links, forms, and other content for toggling -->
+ <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
+ <ul class="nav navbar-nav">
+ %MENUS%
+ </ul>
+ </div><!-- /.navbar-collapse -->
+ </div>
+</nav>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>%TITLE%</title>
+
+ <link href="%ROOT_URL%/assets/vendors/bootstrap/bootstrap-3.2.0-dist/css/bootstrap.min.css" rel="stylesheet">
+ <link href="%ROOT_URL%/assets/css/main.css" rel="stylesheet">
+
+ <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
+ <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
+ <!--[if lt IE 9]>
+ <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
+ <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
+ <![endif]-->
+ </head>
+ <body>
+ %HEADER%
+ %TOP_MENU%
+ <div class="container">
+ <div class="row">
+ %BODY%
+ </div>
+ %FOOTER%
+ </div>
+
+ <script src="%ROOT_URL%/vendors/jquery/jquery-1.11.1.min.js"></script>
+ <script src="%ROOT_URL%/vendors/bootstrap/bootstrap-3.2.0-dist/js/bootstrap.min.js"></script>
+ </body>
+</html>
module doc_model
import model_utils
-import markdown
+import docdown
import doc_templates
import ordered_tree
import model_ext
# limitations under the License.
# Transform Nit verbatim documentation into HTML
-module markdown
+module docdown
private import parser
import html
import modelize
import highlight
-import markdown
+import docdown
redef class ModelBuilder
fun test_markdown(page: HTMLTag, mmodule: MModule)
module testing_doc
import testing_base
-intrude import markdown
+intrude import docdown
# Extractor, Executor and Reporter for the tests in a module
class NitUnitExecutor
-PROGS=*.nit ../examples/*.nit ../examples/leapfrog/leapfrog.nit ../examples/shoot/shoot_logic.nit ../contrib/pep8analysis/src/pep8analysis ../lib/*.nit ../src/nitdoc.nit ../src/test_parser.nit ../src/nit.nit ../src/nitmetrics.nit ../src/nitg.nit
+PROGS=*.nit ../examples/*.nit ../examples/leapfrog/leapfrog.nit ../examples/shoot/shoot_logic.nit ../contrib/pep8analysis/src/pep8analysis ../contrib/nitiwiki/src/nitiwiki ../lib/*.nit ../src/nitdoc.nit ../src/test_parser.nit ../src/nit.nit ../src/nitmetrics.nit ../src/nitg.nit
all: niti nitg-g nitg-s
../lib/*/examples/*.nit \
../contrib/friendz/src/solver_cmd.nit \
../contrib/pep8analysis/src/pep8analysis.nit \
+ ../contrib/nitiwiki/src/nitiwiki.nit \
*.nit
--- /dev/null
+nitiwiki --config ../contrib/nitiwiki/tests/wiki1/config2.ini --clean --status
+nitiwiki --config ../contrib/nitiwiki/tests/wiki1/config2.ini --clean --render -v
--- /dev/null
+Not in a nitiwiki directory.
+Use --init to initialize one here.
--- /dev/null
+nitiWiki
+name: wiki2
+config: ../contrib/nitiwiki/tests/wiki1/config2.ini
+url: http://localhost/
+
+There is modified files:
+ + pages
+ + /pages/index.md
+
+Use nitiwiki --render to render modified files
--- /dev/null
+Render section out