nit: Added link to `CONTRIBUTING.md` from the README
[nit.git] / contrib / nitiwiki / src / wiki_links.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 # Wiki internal links handling.
16 module wiki_links
17
18 import wiki_base
19 import markdown::wikilinks
20 import ordered_tree
21
22 redef class Nitiwiki
23 # Looks up a WikiEntry by its `name`.
24 #
25 # Rules are:
26 # 1. Looks in the current section
27 # 2. Looks in the current section children
28 # 3. Looks in the current section parent
29 # 4. Looks up to wiki root
30 #
31 # Returns `null` if no article can be found.
32 fun lookup_entry_by_name(context: WikiEntry, name: String): nullable WikiEntry do
33 var section: nullable WikiEntry = context.parent or else context
34 var res = section.lookup_entry_by_name(name)
35 if res != null then return res
36 while section != null do
37 if section.name == name then return section
38 if section.children.has_key(name) then return section.children[name]
39 section = section.parent
40 end
41 return null
42 end
43
44 # Looks up a WikiEntry by its `title`.
45 #
46 # Rules are:
47 # 1. Looks in the current section
48 # 2. Looks in the current section children
49 # 3. Looks in the current section parent
50 # 4. Looks up to wiki root
51 #
52 # Returns `null` if no article can be found.
53 fun lookup_entry_by_title(context: WikiEntry, title: String): nullable WikiEntry do
54 var section: nullable WikiEntry = context.parent or else context
55 var res = section.lookup_entry_by_title(title)
56 if res != null then return res
57 while section != null do
58 if section.title.to_lower == title.to_lower then return section
59 for child in section.children.values do
60 if child.title.to_lower == title.to_lower then return child
61 end
62 section = section.parent
63 end
64 return null
65 end
66
67 # Looks up a WikiEntry by its `path`.
68 #
69 # Path can be relative from `context` like `context/entry`.
70 # Or absolute like `/entry1/entry2`.
71 #
72 # Returns `null` if no article can be found.
73 fun lookup_entry_by_path(context: WikiEntry, path: String): nullable WikiEntry do
74 var entry = context.parent or else context
75 var parts = path.split_with("/")
76 if path.has_prefix("/") then
77 entry = root_section
78 if parts.is_empty then return root_section.index
79 parts.shift
80 end
81 while not parts.is_empty do
82 var name = parts.shift
83 if name.is_empty then continue
84 if entry.name == name then continue
85 if not entry.children.has_key(name) then return null
86 entry = entry.children[name]
87 end
88 return entry
89 end
90
91 # Trails between pages
92 #
93 # Trails are represented as a forest of entries.
94 # This way it is possible to represent a flat-trail as a visit of a tree.
95 var trails = new OrderedTree[WikiEntry]
96 end
97
98 redef class WikiEntry
99
100 # Relative path to `self` from the target root_url
101 fun href: String do return breadcrumbs.join("/")
102
103 # Relative path to the directory `self` from the target root_url
104 fun dir_href: String do return href.dirname
105
106 # Relative path to the root url from `self`
107 fun root_href: String do
108 var root_dir = dir_href.relpath("")
109 # Avoid issues if used as a macro just followed by a `/` (as with url prefix)
110 if root_dir == "" then root_dir = "."
111 return root_dir
112 end
113
114 # A relative `href` to `self` from the page `context`.
115 #
116 # Should be used to navigate between documents.
117 fun href_from(context: WikiEntry): String
118 do
119 var res = context.dir_href.relpath(href)
120 return res
121 end
122
123 # A relative hyperlink <a> to `self` from the page `context`.
124 #
125 # If `text` is not given, `title` will be used instead.
126 fun a_from(context: WikiEntry, text: nullable Text): Writable
127 do
128 var title = title.html_escape
129 if text == null then text = title else text = text.html_escape
130 var href = href_from(context)
131 return """<a href="{{{href}}}" title="{{{title}}}">{{{text}}}</a>"""
132 end
133
134 redef fun render do
135 super
136 if not is_dirty and not wiki.force_render then return
137 render_sidebar_wikilinks
138 end
139
140 # Search in `self` then `self.children` if an entry has the name `name`.
141 fun lookup_entry_by_name(name: String): nullable WikiEntry do
142 if children.has_key(name) then return children[name]
143 for child in children.values do
144 var res = child.lookup_entry_by_name(name)
145 if res != null then return res
146 end
147 return null
148 end
149
150 # Search in `self` then `self.children` if an entry has the title `title`.
151 fun lookup_entry_by_title(title: String): nullable WikiEntry do
152 for child in children.values do
153 if child.title.to_lower == title.to_lower then return child
154 end
155 for child in children.values do
156 var res = child.lookup_entry_by_title(title)
157 if res != null then return res
158 end
159 return null
160 end
161
162 private var md_proc: NitiwikiMdProcessor is lazy do
163 return new NitiwikiMdProcessor(wiki, self)
164 end
165
166 # Process wikilinks from sidebar.
167 private fun render_sidebar_wikilinks do
168 var blocks = sidebar.blocks
169 for i in [0..blocks.length[ do
170 blocks[i] = md_proc.process(blocks[i].to_s).write_to_string
171 md_proc.emitter.decorator.headlines.clear
172 end
173 end
174 end
175
176 redef class WikiSection
177
178 # The index page for this section.
179 #
180 # If no file `index.md` exists for this section,
181 # a summary is generated using contained articles.
182 var index: WikiArticle is lazy do
183 for child in children.values do
184 if child isa WikiArticle and child.is_index then return child
185 end
186 return new WikiSectionIndex(wiki, "index", self)
187 end
188
189 redef fun dir_href do return href
190 end
191
192 redef class WikiArticle
193
194 # Headlines ids and titles.
195 var headlines = new ArrayMap[String, HeadLine]
196
197 # Is `self` an index page?
198 #
199 # Checks if `self.name == "index"`.
200 fun is_index: Bool do return name == "index"
201
202 redef fun href do
203 if parent == null then
204 return "{name}.html"
205 else
206 return parent.href.join_path("{name}.html")
207 end
208 end
209
210 redef fun render do
211 super
212 if not is_dirty and not wiki.force_render or not has_source then return
213 content = md_proc.process(md.as(not null))
214 headlines.add_all(md_proc.emitter.decorator.headlines)
215 end
216 end
217
218 # A `WikiArticle` that contains the section index tree.
219 class WikiSectionIndex
220 super WikiArticle
221
222 # The section described by `self`.
223 var section: WikiSection
224
225 redef fun title do return section.title
226
227 redef fun href do return section.href
228
229 redef fun dir_href do return section.dir_href
230 end
231
232 # A MarkdownProcessor able to parse wiki links.
233 class NitiwikiMdProcessor
234 super MarkdownProcessor
235
236 # Wiki used to resolve links.
237 var wiki: Nitiwiki
238
239 # Article parsed by `self`.
240 #
241 # Used to contextualize links.
242 var context: WikiEntry
243
244 init do
245 emitter = new MarkdownEmitter(self)
246 emitter.decorator = new NitiwikiDecorator(wiki, context)
247 end
248 end
249
250 # The decorator associated to `MarkdownProcessor`.
251 class NitiwikiDecorator
252 super HTMLDecorator
253
254 # Wiki used to resolve links.
255 var wiki: Nitiwiki
256
257 # Article used to contextualize links.
258 var context: WikiEntry
259
260 redef fun add_wikilink(v, token) do
261 var wiki = v.processor.as(NitiwikiMdProcessor).wiki
262 var target: nullable WikiEntry = null
263 var anchor: nullable String = null
264 var link = token.link
265 if link == null then return
266 var name = token.name
267 v.add "<a "
268 if not link.has_prefix("http://") and not link.has_prefix("https://") then
269 # Extract commands from the link.
270 var command = null
271 var command_split = link.split_once_on(":")
272 if command_split.length > 1 then
273 command = command_split[0].trim
274 link = command_split[1].trim
275 end
276
277 if link.has("#") then
278 var parts = link.split_with("#")
279 link = parts.first
280 anchor = parts.subarray(1, parts.length - 1).join("#")
281 end
282 if link.has("/") then
283 target = wiki.lookup_entry_by_path(context, link.to_s)
284 else
285 target = wiki.lookup_entry_by_name(context, link.to_s)
286 if target == null then
287 target = wiki.lookup_entry_by_title(context, link.to_s)
288 end
289 end
290 if target != null then
291 if name == null then name = target.title
292 link = target.href_from(context)
293
294 if command == "trail" then
295 if target isa WikiSection then target = target.index
296 wiki.trails.add(context, target)
297 end
298 else
299 wiki.message("Warning: unknown wikilink `{link}` (in {context.src_path.as(not null)})", 0)
300 v.add "class=\"broken\" "
301 end
302 end
303 v.add "href=\""
304 append_value(v, link)
305 if anchor != null then append_value(v, "#{anchor}")
306 v.add "\""
307 var comment = token.comment
308 if comment != null and not comment.is_empty then
309 v.add " title=\""
310 append_value(v, comment)
311 v.add "\""
312 end
313 v.add ">"
314 if name == null then name = link
315 v.emit_text(name)
316 v.add "</a>"
317 end
318 end