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