contrib/nitiwiki: introduce wikilinks
[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 intrude import markdown
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 = context.parent
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 = context.parent
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 == title then return section
58 for child in section.children.values do
59 if child.title == title 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
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 not entry.children.has_key(name) then return null
84 entry = entry.children[name]
85 end
86 return entry
87 end
88 end
89
90 redef class WikiEntry
91
92 # Url to `self` once generated.
93 fun url: String do return wiki.config.root_url.join_path(breadcrumbs.join("/"))
94
95 redef fun render do
96 super
97 if not is_dirty and not wiki.force_render then return
98 end
99
100 # Search in `self` then `self.children` if an entry has the name `name`.
101 fun lookup_entry_by_name(name: String): nullable WikiEntry do
102 if children.has_key(name) then return children[name]
103 for child in children.values do
104 var res = child.lookup_entry_by_name(name)
105 if res != null then return res
106 end
107 return null
108 end
109
110 # Search in `self` then `self.children` if an entry has the title `title`.
111 fun lookup_entry_by_title(title: String): nullable WikiEntry do
112 for child in children.values do
113 if child.title == title then return child
114 end
115 for child in children.values do
116 var res = child.lookup_entry_by_title(title)
117 if res != null then return res
118 end
119 return null
120 end
121 end
122
123 redef class WikiSection
124
125 # The index page for this section.
126 #
127 # If no file `index.md` exists for this section,
128 # a summary is generated using contained articles.
129 var index: WikiArticle is lazy do
130 for child in children.values do
131 if child isa WikiArticle and child.is_index then return child
132 end
133 return new WikiSectionIndex(wiki, "index", self)
134 end
135 end
136
137 redef class WikiArticle
138
139 # Headlines ids and titles.
140 var headlines = new ArrayMap[String, HeadLine]
141
142 # Is `self` an index page?
143 #
144 # Checks if `self.name == "index"`.
145 fun is_index: Bool do return name == "index"
146
147 redef fun url do
148 if parent == null then
149 return wiki.config.root_url.join_path("{name}.html")
150 else
151 return parent.url.join_path("{name}.html")
152 end
153 end
154
155 redef fun render do
156 super
157 if not is_dirty and not wiki.force_render or not has_source then return
158 var md_proc = new NitiwikiMdProcessor(wiki, self)
159 content = md_proc.process(md.as(not null))
160 headlines.recover_with(md_proc.emitter.decorator.headlines)
161 end
162 end
163
164 # A `WikiArticle` that contains the section index tree.
165 class WikiSectionIndex
166 super WikiArticle
167
168 # The section described by `self`.
169 var section: WikiSection
170
171 redef fun title do return section.title
172
173 redef fun url do return section.url
174 end
175
176 # A MarkdownProcessor able to parse wiki links.
177 class NitiwikiMdProcessor
178 super MarkdownProcessor
179
180 # Wiki used to resolve links.
181 var wiki: Nitiwiki
182
183 # Article parsed by `self`.
184 #
185 # Used to contextualize links.
186 var context: WikiArticle
187
188 init do
189 emitter = new MarkdownEmitter(self)
190 emitter.decorator = new NitiwikiDecorator(wiki, context)
191 end
192
193 redef fun token_at(text, pos) do
194 var token = super
195 if not token isa TokenLink then return token
196 if pos + 1 < text.length then
197 var c = text[pos + 1]
198 if c == '[' then return new TokenWikiLink(pos, c)
199 end
200 return token
201 end
202 end
203
204 private class NitiwikiDecorator
205 super HTMLDecorator
206
207 # Wiki used to resolve links.
208 var wiki: Nitiwiki
209
210 # Article used to contextualize links.
211 var context: WikiArticle
212
213 fun add_wikilink(v: MarkdownEmitter, link: Text, name, comment: nullable Text) do
214 var wiki = v.processor.as(NitiwikiMdProcessor).wiki
215 var target: nullable WikiEntry = null
216 var anchor: nullable String = null
217 if link.has("#") then
218 var parts = link.split_with("#")
219 link = parts.first
220 anchor = parts.subarray(1, parts.length - 1).join("#")
221 end
222 if link.has("/") then
223 target = wiki.lookup_entry_by_path(context, link.to_s)
224 else
225 target = wiki.lookup_entry_by_name(context, link.to_s)
226 if target == null then
227 target = wiki.lookup_entry_by_title(context, link.to_s)
228 end
229 end
230 v.add "<a "
231 if target != null then
232 if name == null then name = target.title
233 link = target.url
234 else
235 wiki.message("Warning: unknown wikilink `{link}` (in {context.src_path.as(not null)})", 0)
236 v.add "class=\"broken\" "
237 end
238 v.add "href=\""
239 append_value(v, link)
240 if anchor != null then append_value(v, "#{anchor}")
241 v.add "\""
242 if comment != null and not comment.is_empty then
243 v.add " title=\""
244 append_value(v, comment)
245 v.add "\""
246 end
247 v.add ">"
248 if name == null then name = link
249 v.emit_text(name)
250 v.add "</a>"
251 end
252 end
253
254 # A NitiWiki link token.
255 #
256 # Something of the form `[[foo]]`.
257 #
258 # Allowed formats:
259 #
260 # * `[[Wikilink]]`
261 # * `[[Wikilink/Bar]]`
262 # * `[[Wikilink#foo]]`
263 # * `[[Wikilink/Bar#foo]]`
264 # * `[[title|Wikilink]]`
265 # * `[[title|Wikilink/Bar]]`
266 # * `[[title|Wikilink/Bar#foo]]`
267 class TokenWikiLink
268 super TokenLink
269
270 redef fun emit_hyper(v) do
271 v.decorator.as(NitiwikiDecorator).add_wikilink(v, link.as(not null), name, comment)
272 end
273
274 redef fun check_link(v, out, start, token) do
275 var md = v.current_text
276 var pos = start + 2
277 var tmp = new FlatBuffer
278 pos = md.read_md_link_id(tmp, pos)
279 if pos < start then return -1
280 var name = tmp.write_to_string
281 if name.has("|") then
282 var parts = name.split_once_on("|")
283 self.name = parts.first
284 self.link = parts[1]
285 else
286 self.name = null
287 self.link = name
288 end
289 pos += 1
290 pos = md.skip_spaces(pos)
291 if pos < start then return -1
292 pos += 1
293 return pos
294 end
295 end