Merge: Added contributing guidelines and link from readme
[nit.git] / contrib / nitiwiki / src / wiki_html.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # HTML wiki rendering
16 module wiki_html
17
18 import wiki_links
19 import markdown::decorators
20
21 redef class Nitiwiki
22
23 # Render HTML output looking for changes in the markdown sources.
24 redef fun render do
25 super
26 if not root_section.is_dirty and not force_render then return
27 var out_dir = expand_path(config.root_dir, config.out_dir)
28 out_dir.mkdir
29 copy_assets
30 root_section.add_child make_sitemap
31 root_section.render
32 end
33
34 # Copy the asset directory to the (public) output directory.
35 private fun copy_assets do
36 var src = expand_path(config.root_dir, config.assets_dir)
37 var out = expand_path(config.root_dir, config.out_dir)
38 if need_render(src, expand_path(out, config.assets_dir)) then
39 if src.file_exists then sys.system "cp -R -- {src.escape_to_sh} {out.escape_to_sh}"
40 end
41 end
42
43 # Build the wiki sitemap page.
44 private fun make_sitemap: WikiSitemap do
45 var sitemap = new WikiSitemap(self, "sitemap")
46 sitemap.is_dirty = true
47 return sitemap
48 end
49
50 # Markdown processor used for inline element such as titles in TOC.
51 private var inline_processor: MarkdownProcessor is lazy do
52 var proc = new MarkdownProcessor
53 proc.emitter.decorator = new InlineDecorator
54 return proc
55 end
56
57 # Inline markdown (remove h1, p, ... elements).
58 private fun inline_md(md: Writable): Writable do
59 return inline_processor.process(md.write_to_string)
60 end
61 end
62
63 redef class WikiEntry
64 # Get a `<a>` template link to `self`
65 fun tpl_link(context: WikiEntry): Writable do
66 return "<a href=\"{href_from(context)}\">{title}</a>"
67 end
68 end
69
70 redef class WikiSection
71
72 # Output directory (where to ouput the HTML pages for this section).
73 redef fun out_path do
74 var parent = self.parent
75 if parent == null then
76 return wiki.config.out_dir
77 else
78 return wiki.expand_path(parent.out_path, name)
79 end
80 end
81
82 redef fun render do
83 if not is_dirty and not wiki.force_render then return
84 if is_new then
85 out_full_path.mkdir
86 else
87 sys.system "touch -- {out_full_path.escape_to_sh}"
88 end
89 if has_source then
90 wiki.message("Render section {name} -> {out_path}", 1)
91 copy_files
92 end
93 var index = self.index
94 if index isa WikiSectionIndex then
95 wiki.message("Render auto-index for section {name} -> {out_path}", 1)
96 index.is_dirty = true
97 add_child index
98 end
99 # Hack: Force the rendering of `index` first so that trails are collected
100 # TODO: Add first-pass analysis to collect global information before doing the rendering
101 index.render
102 super
103 end
104
105 # Copy attached files from `src_path` to `out_path`.
106 private fun copy_files do
107 assert has_source
108 var dir = src_full_path.as(not null).to_s
109 for name in dir.files do
110 if name == wiki.config_filename then continue
111 if name.has_suffix(".md") then continue
112 if has_child(name) then continue
113 var src = wiki.expand_path(dir, name)
114 var out = wiki.expand_path(out_full_path, name)
115 if not wiki.need_render(src, out) then continue
116 sys.system "cp -R -- {src.escape_to_sh} {out_full_path.escape_to_sh}"
117 end
118 end
119
120 redef fun tpl_link(context) do return index.tpl_link(context)
121
122 # Render the section hierarchy as a html tree.
123 #
124 # `limit` is used to specify the max-depth of the tree.
125 #
126 # The generated tree will be something like this:
127 #
128 # ~~~html
129 # <ul>
130 # <li>section 1</li>
131 # <li>section 2
132 # <ul>
133 # <li>section 2.1</li>
134 # <li>section 2.2</li>
135 # </ul>
136 # </li>
137 # </ul>
138 # ~~~
139 fun tpl_tree(limit: Int): Template do
140 return tpl_tree_intern(limit, 1, self)
141 end
142
143 # Build the template tree for this section recursively.
144 protected fun tpl_tree_intern(limit, count: Int, context: WikiEntry): Template do
145 var out = new Template
146 var index = index
147 out.add "<li>"
148 out.add tpl_link(context)
149 if (limit < 0 or count < limit) and
150 (children.length > 1 or (children.length == 1)) then
151 out.add " <ul>"
152 for child in children.values do
153 if child == index then continue
154 if child isa WikiArticle then
155 out.add "<li>"
156 out.add child.tpl_link(context)
157 out.add "</li>"
158 else if child isa WikiSection and not child.is_hidden then
159 out.add child.tpl_tree_intern(limit, count + 1, context)
160 end
161 end
162 out.add " </ul>"
163 end
164 out.add "</li>"
165 return out
166 end
167 end
168
169 redef class WikiArticle
170
171 redef fun out_path do
172 var parent = self.parent
173 if parent == null then
174 return wiki.expand_path(wiki.config.out_dir, "{name}.html")
175 else
176 return wiki.expand_path(parent.out_path, "{name}.html")
177 end
178 end
179
180 redef fun render do
181 super
182 if not is_dirty and not wiki.force_render then return
183 var file = out_full_path
184 wiki.message("Render article {name} -> {file}", 1)
185 file.dirname.mkdir
186 tpl_page.write_to_file file
187 end
188
189
190 # Load a template and resolve page-related macros
191 fun load_template(template_file: String): TemplateString do
192 var tpl = wiki.load_template(template_file)
193 if tpl.has_macro("ROOT_URL") then
194 tpl.replace("ROOT_URL", root_href)
195 end
196 return tpl
197 end
198
199 # Replace macros in the template by wiki data.
200 private fun tpl_page: TemplateString do
201 var tpl = load_template(template_file)
202 if tpl.has_macro("TOP_MENU") then
203 tpl.replace("TOP_MENU", tpl_menu)
204 end
205 if tpl.has_macro("HEADER") then
206 tpl.replace("HEADER", tpl_header)
207 end
208 if tpl.has_macro("BODY") then
209 tpl.replace("BODY", tpl_article)
210 end
211 if tpl.has_macro("FOOTER") then
212 tpl.replace("FOOTER", tpl_footer)
213 end
214 if tpl.has_macro("TRAIL") then
215 tpl.replace("TRAIL", tpl_trail)
216 end
217 return tpl
218 end
219
220 # Generate the HTML header for this article.
221 fun tpl_header: Writable do
222 var file = header_file
223 if not wiki.has_template(file) then return ""
224 return load_template(file)
225 end
226
227 # Generate the HTML page for this article.
228 fun tpl_article: TplArticle do
229 var article = new TplArticle
230 article.body = content
231 if wiki.config.auto_breadcrumbs then
232 article.breadcrumbs = new TplBreadcrumbs(self)
233 end
234 article.sidebar = tpl_sidebar
235 article.sidebar_pos = wiki.config.sidebar
236 return article
237 end
238
239 # Sidebar for this page.
240 var tpl_sidebar: TplSidebar is lazy do
241 var res = new TplSidebar
242 if wiki.config.auto_summary then
243 res.blocks.add tpl_summary
244 end
245 res.blocks.add_all sidebar.blocks
246 return res
247 end
248
249 # Generate the HTML summary for this article.
250 #
251 # Based on `headlines`
252 fun tpl_summary: Writable do
253 var headlines = self.headlines
254 var tpl = new Template
255 tpl.add "<ul class=\"summary list-unstyled\">"
256 var iter = headlines.iterator
257 while iter.is_ok do
258 var hl = iter.item
259 # parse title as markdown
260 var title = wiki.inline_md(hl.title)
261 tpl.add "<li><a href=\"#{hl.id}\">{title}</a>"
262 iter.next
263 if iter.is_ok then
264 if iter.item.level > hl.level then
265 tpl.add "<ul class=\"list-unstyled\">"
266 else if iter.item.level < hl.level then
267 tpl.add "</li>"
268 tpl.add "</ul>"
269 end
270 else
271 tpl.add "</li>"
272 end
273 end
274 tpl.add "</ul>"
275 return tpl
276 end
277
278 # Generate the HTML menu for this article.
279 fun tpl_menu: Writable do
280 var file = menu_file
281 if not wiki.has_template(file) then return ""
282 var tpl = load_template(file)
283 if tpl.has_macro("MENUS") then
284 var items = new Template
285 for child in wiki.root_section.children.values do
286 if child isa WikiArticle and child.is_index then continue
287 if child isa WikiSection and child.is_hidden then continue
288 items.add "<li"
289 if self == child or self.breadcrumbs.has(child) then
290 items.add " class=\"active\""
291 end
292 items.add ">"
293 items.add child.tpl_link(self)
294 items.add "</li>"
295 end
296 tpl.replace("MENUS", items)
297 end
298 return tpl
299 end
300
301 # Generate navigation links for the trail of this article, if any.
302 #
303 # A trail is generated if the article include or is included in a trail.
304 # See `wiki.trails` for details.
305 fun tpl_trail: Writable do
306 if not wiki.trails.has(self) then return ""
307
308 # Get the position of `self` in the trail
309 var flat = wiki.trails.to_a
310 var pos = flat.index_of(self)
311 assert pos >= 0
312
313 var res = new Template
314 res.add "<ul class=\"trail\">"
315 var parent = wiki.trails.parent(self)
316 # Up and prev are disabled on a root
317 if parent != null then
318 if pos > 0 then
319 var target = flat[pos-1]
320 res.add "<li>{target.a_from(self, "prev")}</li>"
321 end
322 res.add "<li>{parent.a_from(self, "up")}</li>"
323 end
324 if pos < flat.length - 1 then
325 var target = flat[pos+1]
326 # Only print the next if it is not a root
327 if target.parent != null then
328 res.add "<li>{target.a_from(self, "next")}</li>"
329 end
330 end
331 res.add "</ul>"
332
333 return res
334 end
335
336 # Generate the HTML footer for this article.
337 fun tpl_footer: Writable do
338 var file = footer_file
339 if not wiki.has_template(file) then return ""
340 var tpl = load_template(file)
341 var time = new Tm.gmtime
342 if tpl.has_macro("YEAR") then
343 tpl.replace("YEAR", (time.year + 1900).to_s)
344 end
345 if tpl.has_macro("GEN_TIME") then
346 tpl.replace("GEN_TIME", time.to_s)
347 end
348 if tpl.has_macro("LAST_CHANGES") then
349 var url = "{wiki.config.last_changes}{src_path or else ""}"
350 tpl.replace("LAST_CHANGES", url)
351 end
352 if tpl.has_macro("EDIT") then
353 var url = "{wiki.config.edit}{src_path or else ""}"
354 tpl.replace("EDIT", url)
355 end
356 return tpl
357 end
358 end
359
360 # A `WikiArticle` that contains the sitemap tree.
361 class WikiSitemap
362 super WikiArticle
363
364 redef fun tpl_article do
365 var article = new TplArticle.with_title("Sitemap")
366 article.body = new TplPageTree(wiki.root_section, -1)
367 return article
368 end
369
370 redef var is_dirty = false
371 end
372
373 # A `WikiArticle` that contains the section index tree.
374 redef class WikiSectionIndex
375
376 redef var is_dirty = false
377
378 redef fun tpl_article do
379 var article = new TplArticle.with_title(section.title)
380 article.body = new TplPageTree(section, -1)
381 article.breadcrumbs = new TplBreadcrumbs(self)
382 return article
383 end
384 end
385
386 # Article HTML output.
387 class TplArticle
388 super Template
389
390 # Article title.
391 var title: nullable Writable = null
392
393 # Article HTML body.
394 var body: nullable Writable = null
395
396 # Sidebar of this article (if any).
397 var sidebar: nullable TplSidebar = null
398
399 # Position of the sidebar.
400 #
401 # See `WikiConfig::sidebar`.
402 var sidebar_pos: String = "left"
403
404 # Breadcrumbs from wiki root to this article.
405 var breadcrumbs: nullable TplBreadcrumbs = null
406
407 # Init `self` with a `title`.
408 init with_title(title: Writable) do
409 self.title = title
410 end
411
412 redef fun rendering do
413 if sidebar_pos == "left" then render_sidebar
414 if sidebar == null then
415 add "<div class=\"col-sm-12 content\">"
416 else
417 add "<div class=\"col-sm-9 content\">"
418 end
419 if body != null then
420 add "<article>"
421 if breadcrumbs != null then
422 add breadcrumbs.as(not null)
423 end
424 if title != null then
425 add "<h1>"
426 add title.as(not null)
427 add "</h1>"
428 end
429 add body.as(not null)
430 add " </article>"
431 end
432 add "</div>"
433 if sidebar_pos == "right" then render_sidebar
434 end
435
436 private fun render_sidebar do
437 if sidebar == null then return
438 add "<div class=\"col-sm-3 sidebar\">"
439 add sidebar.as(not null)
440 add "</div>"
441 end
442 end
443
444 # A collection of HTML blocks displayed on the side of a page.
445 class TplSidebar
446 super Template
447
448 # Blocks are `Stremable` pieces that will be rendered in the sidebar.
449 var blocks = new Array[Writable]
450
451 redef fun rendering do
452 for block in blocks do
453 add "<nav class=\"sideblock\">"
454 add block
455 add "</nav>"
456 end
457 end
458 end
459
460 # An HTML breadcrumbs that show the path from a `WikiArticle` to the `Nitiwiki` root.
461 class TplBreadcrumbs
462 super Template
463
464 # Bread crumb article.
465 var article: WikiArticle
466
467 redef fun rendering do
468 var path = article.breadcrumbs
469 if path.is_empty or path.length <= 2 and article.is_index then return
470 add "<ol class=\"breadcrumb\">"
471 for entry in path do
472 if entry == path.last then
473 add "<li class=\"active\">"
474 add entry.title
475 add "</li>"
476 else
477 if article.parent == entry and article.is_index then continue
478 add "<li>"
479 add entry.tpl_link(article)
480 add "</li>"
481 end
482 end
483 add "</ol>"
484 end
485 end
486
487 # An HTML tree that show the section pages structure.
488 class TplPageTree
489 super Template
490
491 # Builds the page tree from `root`.
492 var root: WikiSection
493
494 # Limits the tree depth to `max_depth` levels.
495 var max_depth: Int
496
497 redef fun rendering do
498 add "<ul>"
499 add root.tpl_tree(-1)
500 add "</ul>"
501 end
502 end