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