bbdc9dbd4f02679ee1c66a7b52e14e2f78ca4ad0
[nit.git] / src / web / api_docdown.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 # Nitdoc specific Markdown format handling for Nitweb
16 module api_docdown
17
18 import api_graph
19 intrude import doc_down
20 intrude import markdown::wikilinks
21 import doc_commands
22 import model::model_index
23
24 redef class NitwebConfig
25 # Specific Markdown processor to use within Nitweb
26 var md_processor: MarkdownProcessor is lazy do
27 var proc = new MarkdownProcessor
28 proc.emitter.decorator = new NitwebDecorator(view, modelbuilder)
29 return proc
30 end
31 end
32
33 redef class APIRouter
34 redef init do
35 super
36 use("/docdown/", new APIDocdown(config))
37 end
38 end
39
40 # Docdown handler accept docdown as POST data and render it as HTML
41 class APIDocdown
42 super APIHandler
43
44 redef fun post(req, res) do
45 res.html config.md_processor.process(req.body)
46 end
47 end
48
49 # Specific Markdown decorator for Nitweb
50 #
51 # We reuse all the implementation of the NitdocDecorator and add the wikilinks handling.
52 class NitwebDecorator
53 super NitdocDecorator
54
55 # View used by wikilink commands to find model entities
56 var view: ModelView
57
58 # Modelbuilder used to access code
59 var modelbuilder: ModelBuilder
60
61 redef fun add_span_code(v, buffer, from, to) do
62 var text = new FlatBuffer
63 buffer.read(text, from, to)
64 var name = text.write_to_string
65 name = name.replace("nullable ", "")
66 var mentity = try_find_mentity(view, name)
67 if mentity == null then
68 super
69 else
70 v.add "<code>"
71 v.write_mentity_link(mentity, text.write_to_string)
72 v.add "</code>"
73 end
74 end
75
76 private fun try_find_mentity(view: ModelView, text: String): nullable MEntity do
77 var mentity = view.mentity_by_full_name(text)
78 if mentity != null then return mentity
79
80 var mentities = view.mentities_by_name(text)
81 if mentities.is_empty then
82 return null
83 else if mentities.length > 1 then
84 # TODO smart resolve conflicts
85 end
86 return mentities.first
87 end
88
89 redef fun add_wikilink(v, token) do
90 var link = token.link
91 if link == null then return
92 var cmd = new DocCommand(link.write_to_string)
93 cmd.render(v, token, view)
94 end
95 end
96
97 # Same as `InlineDecorator` but with wikilink commands handling
98 class NitwebInlineDecorator
99 super InlineDecorator
100
101 # View used by wikilink commands to find model entities
102 var view: ModelView
103
104 # Modelbuilder used to access code
105 var modelbuilder: ModelBuilder
106
107 redef fun add_wikilink(v, token) do
108 var link = token.link
109 if link == null then return
110 var cmd = new DocCommand(link.write_to_string)
111 cmd.render(v, token, view)
112 end
113 end
114
115 redef class MarkdownEmitter
116 # Find the MEntity that matches `name`.
117 #
118 # Write an error if the entity is not found
119 fun find_mentity(model: ModelView, name: nullable String): nullable MEntity do
120 if name == null then
121 write_error("No MEntity found")
122 return null
123 end
124 # Lookup by full name
125 var mentity = model.mentity_by_full_name(name)
126 if mentity != null then return mentity
127
128 var mentities = model.mentities_by_name(name)
129 if mentities.is_empty then
130 var suggest = model.find(name, 3)
131 var msg = new Buffer
132 msg.append "No MEntity found for name `{name}`"
133 if suggest.not_empty then
134 msg.append " (suggestions: "
135 var i = 0
136 for s in suggest do
137 msg.append "`{s.full_name}`"
138 if i < suggest.length - 1 then msg.append ", "
139 i += 1
140 end
141 msg.append ")"
142 end
143 write_error(msg.write_to_string)
144 return null
145 else if mentities.length > 1 then
146 var msg = new Buffer
147 msg.append "Conflicts for name `{name}`"
148 msg.append " (conflicts: "
149 var i = 0
150 for s in mentities do
151 msg.append "`{s.full_name}`"
152 if i < mentities.length - 1 then msg.append ", "
153 i += 1
154 end
155 msg.append ")"
156 write_warning(msg.write_to_string)
157 end
158 return mentities.first
159 end
160
161 # Write a warning in the output
162 fun write_warning(text: String) do
163 emit_text "<p class='text-warning'>Warning: {text}</p>"
164 end
165
166 # Write an error in the output
167 fun write_error(text: String) do
168 emit_text "<p class='text-danger'>Error: {text}</p>"
169 end
170
171 # Write a link to a mentity in the output
172 fun write_mentity_link(mentity: MEntity, text: nullable String) do
173 var link = mentity.web_url
174 var name = text or else mentity.name
175 var mdoc = mentity.mdoc_or_fallback
176 var comment = null
177 if mdoc != null then comment = mdoc.synopsis
178 decorator.add_link(self, link, name, comment)
179 end
180
181 # Write a mentity list in the output
182 fun write_mentity_list(mentities: Collection[MEntity]) do
183 add "<ul>"
184 for mentity in mentities do
185 var mdoc = mentity.mdoc_or_fallback
186 add "<li>"
187 write_mentity_link(mentity)
188 if mdoc != null then
189 add " - "
190 emit_text mdoc.synopsis
191 end
192 add "</li>"
193 end
194 add "</ul>"
195 end
196 end
197
198 redef interface DocCommand
199
200 # Emit the HTML related to the execution of this doc command
201 fun render(v: MarkdownEmitter, token: TokenWikiLink, model: ModelView) do
202 v.write_error("Not yet implemented command `{token.link or else "null"}`")
203 end
204 end
205
206 redef class UnknownCommand
207 redef fun render(v, token, model) do
208 var link = token.link
209 if link == null then
210 v.write_error("Empty command")
211 return
212 end
213 var full_name = link.write_to_string
214 var mentity = v.find_mentity(model, full_name)
215 if mentity == null then return
216 v.write_mentity_link(mentity)
217 end
218 end
219
220 redef class ArticleCommand
221 redef fun render(v, token, model) do
222 if args.is_empty then
223 v.write_error("Expected one arg: the MEntity name")
224 return
225 end
226 var name = args.first
227 var mentity = v.find_mentity(model, name)
228 if mentity == null then return
229 var mdoc = mentity.mdoc_or_fallback
230 if mdoc == null then
231 v.write_warning("No MDoc for mentity `{name}`")
232 return
233 end
234 v.add "<h3>"
235 v.write_mentity_link(mentity)
236 v.add " - "
237 v.emit_text mdoc.synopsis
238 v.add "</h3>"
239 v.add v.processor.process(mdoc.comment).write_to_string
240 end
241 end
242
243 redef class CommentCommand
244 redef fun render(v, token, model) do
245 if args.is_empty then
246 v.write_error("Expected one arg: the MEntity name")
247 return
248 end
249 var name = args.first
250 var mentity = v.find_mentity(model, name)
251 if mentity == null then return
252 var mdoc = mentity.mdoc_or_fallback
253 if mdoc == null then
254 v.write_warning("No MDoc for mentity `{name}`")
255 return
256 end
257 v.add v.processor.process(mdoc.comment).write_to_string
258 end
259 end
260
261 redef class ListCommand
262 redef fun render(v, token, model) do
263 if args.is_empty then
264 v.write_error("Expected one arg: the MEntity name")
265 return
266 end
267 var name = args.first
268 var mentity = v.find_mentity(model, name)
269 if mentity == null then return
270 if mentity isa MPackage then
271 v.write_mentity_list(mentity.mgroups)
272 else if mentity isa MGroup then
273 var res = new Array[MEntity]
274 res.add_all mentity.in_nesting.smallers
275 res.add_all mentity.mmodules
276 v.write_mentity_list(res)
277 else if mentity isa MModule then
278 v.write_mentity_list(mentity.mclassdefs)
279 else if mentity isa MClass then
280 v.write_mentity_list(mentity.collect_intro_mproperties(model))
281 else if mentity isa MClassDef then
282 v.write_mentity_list(mentity.mpropdefs)
283 else if mentity isa MProperty then
284 v.write_mentity_list(mentity.mpropdefs)
285 else
286 v.write_error("No list found for name `{name}`")
287 end
288 end
289 end
290
291 redef class CodeCommand
292 redef fun render(v, token, model) do
293 if args.is_empty then
294 v.write_error("Expected one arg: the MEntity name")
295 return
296 end
297 var name = args.first
298 var mentity = v.find_mentity(model, name)
299 if mentity == null then return
300 if mentity isa MClass then mentity = mentity.intro
301 if mentity isa MProperty then mentity = mentity.intro
302 var source = render_source(mentity, v.decorator.as(NitwebDecorator).modelbuilder)
303 if source == null then
304 v.write_error("No source for MEntity `{name}`")
305 return
306 end
307 v.add "<pre>"
308 v.add source
309 v.add "</pre>"
310 end
311
312 # Highlight `mentity` source code.
313 private fun render_source(mentity: MEntity, modelbuilder: ModelBuilder): nullable HTMLTag do
314 var node = modelbuilder.mentity2node(mentity)
315 if node == null then return null
316 var hl = new HighlightVisitor
317 hl.enter_visit node
318 return hl.html
319 end
320 end
321
322 redef class GraphCommand
323 redef fun render(v, token, model) do
324 if args.is_empty then
325 v.write_error("Expected one arg: the MEntity name")
326 return
327 end
328 var name = args.first
329 var mentity = v.find_mentity(model, name)
330 if mentity == null then return
331 var g = new InheritanceGraph(mentity, model)
332 v.add g.draw(3, 3).to_svg
333 end
334 end
335
336 redef class Text
337 # Read `self` between `nstart` and `nend` (excluded) and writte chars to `out`.
338 private fun read(out: FlatBuffer, nstart, nend: Int): Int do
339 var pos = nstart
340 while pos < length and pos < nend do
341 out.add self[pos]
342 pos += 1
343 end
344 if pos == length then return -1
345 return pos
346 end
347 end