contrib/nitiwiki: move `render` to `wiki_base`
[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_base
19
20 redef class Nitiwiki
21
22 # Render HTML output looking for changes in the markdown sources.
23 redef fun render do
24 super
25 if not root_section.is_dirty and not force_render then return
26 var out_dir = expand_path(config.root_dir, config.out_dir)
27 out_dir.mkdir
28 copy_assets
29 root_section.add_child make_sitemap
30 root_section.render
31 end
32
33 # Copy the asset directory to the (public) output directory.
34 private fun copy_assets do
35 var src = expand_path(config.root_dir, config.assets_dir)
36 var out = expand_path(config.root_dir, config.out_dir)
37 if need_render(src, expand_path(out, config.assets_dir)) then
38 if src.file_exists then sys.system "cp -R {src} {out}"
39 end
40 end
41
42 # Build the wiki sitemap page.
43 private fun make_sitemap: WikiSitemap do
44 var sitemap = new WikiSitemap(self, "sitemap")
45 sitemap.is_dirty = true
46 return sitemap
47 end
48 end
49
50 redef class WikiEntry
51
52 # Url to `self` once generated.
53 fun url: String do return wiki.config.root_url.join_path(breadcrumbs.join("/"))
54
55 # Get a `<a>` template link to `self`
56 fun tpl_link: Writable do
57 return "<a href=\"{url}\">{title}</a>"
58 end
59 end
60
61 redef class WikiSection
62
63 # Output directory (where to ouput the HTML pages for this section).
64 redef fun out_path: String do
65 if parent == null then
66 return wiki.config.out_dir
67 else
68 return wiki.expand_path(parent.out_path, name)
69 end
70 end
71
72 redef fun render do
73 if not is_dirty and not wiki.force_render then return
74 if is_new then
75 out_full_path.mkdir
76 else
77 sys.system "touch {out_full_path}"
78 end
79 if has_source then
80 wiki.message("Render section {out_path}", 1)
81 copy_files
82 end
83 var index = self.index
84 if index isa WikiSectionIndex then
85 wiki.message("Render auto-index for section {out_path}", 1)
86 index.is_dirty = true
87 add_child index
88 end
89 super
90 end
91
92 # Copy attached files from `src_path` to `out_path`.
93 private fun copy_files do
94 assert has_source
95 var dir = src_full_path.to_s
96 for name in dir.files do
97 if name == wiki.config_filename then continue
98 if name.has_suffix(".md") then continue
99 if has_child(name) then continue
100 var src = wiki.expand_path(dir, name)
101 var out = wiki.expand_path(out_full_path, name)
102 if not wiki.need_render(src, out) then continue
103 sys.system "cp -R {src} {out_full_path}"
104 end
105 end
106
107 # The index page for this section.
108 #
109 # If no file `index.md` exists for this section,
110 # a summary is generated using contained articles.
111 var index: WikiArticle is lazy do
112 for child in children.values do
113 if child isa WikiArticle and child.is_index then return child
114 end
115 return new WikiSectionIndex(wiki, "index", self)
116 end
117
118 redef fun tpl_link do return index.tpl_link
119
120 # Render the section hierarchy as a html tree.
121 #
122 # `limit` is used to specify the max-depth of the tree.
123 #
124 # The generated tree will be something like this:
125 #
126 # ~~~html
127 # <ul>
128 # <li>section 1</li>
129 # <li>section 2
130 # <ul>
131 # <li>section 2.1</li>
132 # <li>section 2.2</li>
133 # </ul>
134 # </li>
135 # </ul>
136 # ~~~
137 fun tpl_tree(limit: Int): Template do
138 return tpl_tree_intern(limit, 1)
139 end
140
141 # Build the template tree for this section recursively.
142 protected fun tpl_tree_intern(limit, count: Int): Template do
143 var out = new Template
144 var index = index
145 out.add "<li>"
146 out.add tpl_link
147 if (limit < 0 or count < limit) and
148 (children.length > 1 or (children.length == 1)) then
149 out.add " <ul>"
150 for child in children.values do
151 if child == index then continue
152 if child isa WikiArticle then
153 out.add "<li>"
154 out.add child.tpl_link
155 out.add "</li>"
156 else if child isa WikiSection and not child.is_hidden then
157 out.add child.tpl_tree_intern(limit, count + 1)
158 end
159 end
160 out.add " </ul>"
161 end
162 out.add "</li>"
163 return out
164 end
165 end
166
167 redef class WikiArticle
168
169 redef fun out_path: String do
170 if parent == null then
171 return wiki.expand_path(wiki.config.out_dir, "{name}.html")
172 else
173 return wiki.expand_path(parent.out_path, "{name}.html")
174 end
175 end
176
177 redef fun url do
178 if parent == null then
179 return wiki.config.root_url.join_path("{name}.html")
180 else
181 return parent.url.join_path("{name}.html")
182 end
183 end
184
185 # Is `self` an index page?
186 #
187 # Checks if `self.name == "index"`.
188 fun is_index: Bool do return name == "index"
189
190 redef fun render do
191 if not is_dirty and not wiki.force_render then return
192 wiki.message("Render article {name}", 2)
193 var file = out_full_path
194 file.dirname.mkdir
195 tpl_page.write_to_file file
196 super
197 end
198
199
200 # Replace macros in the template by wiki data.
201 private fun tpl_page: TemplateString do
202 var tpl = wiki.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 return tpl
216 end
217
218 # Generate the HTML header for this article.
219 fun tpl_header: Writable do
220 var file = header_file
221 if not wiki.has_template(file) then return ""
222 return wiki.load_template(file)
223 end
224
225 # Generate the HTML page for this article.
226 fun tpl_article: TplArticle do
227 var article = new TplArticle
228 article.body = content
229 article.breadcrumbs = new TplBreadcrumbs(self)
230 tpl_sidebar.blocks.add tpl_summary
231 article.sidebar = tpl_sidebar
232 return article
233 end
234
235 # Sidebar for this page.
236 var tpl_sidebar = new TplSidebar
237
238 # Generate the HTML summary for this article.
239 #
240 # Based on `headlines`
241 fun tpl_summary: Writable do
242 var headlines = self.headlines
243 var tpl = new Template
244 tpl.add "<ul class=\"summary list-unstyled\">"
245 var iter = headlines.iterator
246 while iter.is_ok do
247 var hl = iter.item
248 # parse title as markdown
249 var title = hl.title.md_to_html.to_s
250 title = title.substring(3, title.length - 8)
251 tpl.add "<li><a href=\"#{hl.id}\">{title}</a>"
252 iter.next
253 if iter.is_ok then
254 if iter.item.level > hl.level then
255 tpl.add "<ul class=\"list-unstyled\">"
256 else if iter.item.level < hl.level then
257 tpl.add "</li>"
258 tpl.add "</ul>"
259 end
260 else
261 tpl.add "</li>"
262 end
263 end
264 tpl.add "</ul>"
265 return tpl
266 end
267
268 # Generate the HTML menu for this article.
269 fun tpl_menu: Writable do
270 var file = menu_file
271 if not wiki.has_template(file) then return ""
272 var tpl = wiki.load_template(file)
273 if tpl.has_macro("MENUS") then
274 var items = new Template
275 for child in wiki.root_section.children.values do
276 if child isa WikiArticle and child.is_index then continue
277 if child isa WikiSection and child.is_hidden then continue
278 items.add "<li"
279 if self == child or self.breadcrumbs.has(child) then
280 items.add " class=\"active\""
281 end
282 items.add ">"
283 items.add child.tpl_link
284 items.add "</li>"
285 end
286 tpl.replace("MENUS", items)
287 end
288 return tpl
289 end
290
291 # Generate the HTML footer for this article.
292 fun tpl_footer: Writable do
293 var file = footer_file
294 if not wiki.has_template(file) then return ""
295 var tpl = wiki.load_template(file)
296 var time = new Tm.gmtime
297 if tpl.has_macro("YEAR") then
298 tpl.replace("YEAR", (time.year + 1900).to_s)
299 end
300 if tpl.has_macro("GEN_TIME") then
301 tpl.replace("GEN_TIME", time.to_s)
302 end
303 return tpl
304 end
305 end
306
307 # A `WikiArticle` that contains the sitemap tree.
308 class WikiSitemap
309 super WikiArticle
310
311 redef fun tpl_article do
312 var article = new TplArticle.with_title("Sitemap")
313 article.body = new TplPageTree(wiki.root_section, -1)
314 return article
315 end
316
317 redef var is_dirty = false
318 end
319
320 # A `WikiArticle` that contains the section index tree.
321 class WikiSectionIndex
322 super WikiArticle
323
324 # The section described by `self`.
325 var section: WikiSection
326
327 redef fun title do return section.title
328
329 redef fun url do return section.url
330
331 redef var is_dirty = false
332
333 redef fun tpl_article do
334 var article = new TplArticle.with_title(section.title)
335 article.body = new TplPageTree(section, -1)
336 article.breadcrumbs = new TplBreadcrumbs(self)
337 return article
338 end
339 end
340
341 # Article HTML output.
342 class TplArticle
343 super Template
344
345 # Article title.
346 var title: nullable Writable = null
347
348 # Article HTML body.
349 var body: nullable Writable = null
350
351 # Sidebar of this article (if any).
352 var sidebar: nullable TplSidebar = null
353
354 # Breadcrumbs from wiki root to this article.
355 var breadcrumbs: nullable TplBreadcrumbs = null
356
357 # Init `self` with a `title`.
358 init with_title(title: Writable) do
359 self.title = title
360 end
361
362 redef fun rendering do
363 if sidebar != null then
364 add "<div class=\"col-sm-3 sidebar\">"
365 add sidebar.as(not null)
366 add "</div>"
367 add "<div class=\"col-sm-9 content\">"
368 else
369 add "<div class=\"col-sm-12 content\">"
370 end
371 if body != null then
372 add "<article>"
373 if breadcrumbs != null then
374 add breadcrumbs.as(not null)
375 end
376 if title != null then
377 add "<h1>"
378 add title.as(not null)
379 add "</h1>"
380 end
381 add body.as(not null)
382 add " </article>"
383 end
384 add "</div>"
385 end
386 end
387
388 # A collection of HTML blocks displayed on the side of a page.
389 class TplSidebar
390 super Template
391
392 # Blocks are `Stremable` pieces that will be rendered in the sidebar.
393 var blocks = new Array[Writable]
394
395 redef fun rendering do
396 for block in blocks do
397 add "<div class=\"sideblock\">"
398 add block
399 add "</div>"
400 end
401 end
402 end
403
404 # An HTML breadcrumbs that show the path from a `WikiArticle` to the `Nitiwiki` root.
405 class TplBreadcrumbs
406 super Template
407
408 # Bread crumb article.
409 var article: WikiArticle
410
411 redef fun rendering do
412 var path = article.breadcrumbs
413 if path.is_empty or path.length <= 2 and article.is_index then return
414 add "<ol class=\"breadcrumb\">"
415 for entry in path do
416 if entry == path.last then
417 add "<li class=\"active\">"
418 add entry.title
419 add "</li>"
420 else
421 if article.parent == entry and article.is_index then continue
422 add "<li>"
423 add entry.tpl_link
424 add "</li>"
425 end
426 end
427 add "</ol>"
428 end
429 end
430
431 # An HTML tree that show the section pages structure.
432 class TplPageTree
433 super Template
434
435 # Builds the page tree from `root`.
436 var root: WikiSection
437
438 # Limits the tree depth to `max_depth` levels.
439 var max_depth: Int
440
441 redef fun rendering do
442 add "<ul>"
443 add root.tpl_tree(-1)
444 add "</ul>"
445 end
446 end