lib/markdown: merge processor and emitter
[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.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 v.render_wikilink(token, view)
91 end
92 end
93
94 # Same as `InlineDecorator` but with wikilink commands handling
95 class NitwebInlineDecorator
96 super InlineDecorator
97
98 # View used by wikilink commands to find model entities
99 var view: ModelView
100
101 # Modelbuilder used to access code
102 var modelbuilder: ModelBuilder
103
104 redef fun add_wikilink(v, token) do
105 v.render_wikilink(token, view)
106 end
107 end
108
109 redef class MarkdownProcessor
110
111 # Parser used to process doc commands
112 var parser = new DocCommandParser
113
114 # Render a wikilink
115 fun render_wikilink(token: TokenWikiLink, model: ModelView) do
116 var link = token.link
117 if link == null then return
118 var name = token.name
119 if name != null then link = "{name} | {link}"
120 var cmd = parser.parse(link.write_to_string)
121 if cmd == null then
122 var full_name = if token.link != null then token.link.as(not null).write_to_string.trim else null
123 if full_name == null or full_name.is_empty then
124 write_error("empty wikilink")
125 return
126 end
127 var mentity = find_mentity(model, full_name)
128 if mentity == null then return
129 name = if token.name != null then token.name.as(not null).to_s else null
130 write_mentity_link(mentity, name)
131 return
132 else
133 for message in parser.errors do
134 if message.level == 1 then
135 write_error(message.message)
136 else if message.level > 1 then
137 write_warning(message.message)
138 end
139 end
140 end
141 cmd.render(self, token, model)
142 end
143
144 # Find the MEntity that matches `name`.
145 #
146 # Write an error if the entity is not found
147 fun find_mentity(model: ModelView, name: nullable String): nullable MEntity do
148 if name == null then
149 write_error("no MEntity found")
150 return null
151 end
152 # Lookup by full name
153 var mentity = model.mentity_by_full_name(name)
154 if mentity != null then return mentity
155
156 var mentities = model.mentities_by_name(name)
157 if mentities.is_empty then
158 var suggest = model.find(name, 3)
159 var msg = new Buffer
160 msg.append "no MEntity found for name `{name}`"
161 if suggest.not_empty then
162 msg.append " (suggestions: "
163 var i = 0
164 for s in suggest do
165 msg.append "`{s.full_name}`"
166 if i < suggest.length - 1 then msg.append ", "
167 i += 1
168 end
169 msg.append ")"
170 end
171 write_error(msg.write_to_string)
172 return null
173 else if mentities.length > 1 then
174 var msg = new Buffer
175 msg.append "conflicts for name `{name}`"
176 msg.append " (conflicts: "
177 var i = 0
178 for s in mentities do
179 msg.append "`{s.full_name}`"
180 if i < mentities.length - 1 then msg.append ", "
181 i += 1
182 end
183 msg.append ")"
184 write_warning(msg.write_to_string)
185 end
186 return mentities.first
187 end
188
189 # Write a warning in the output
190 fun write_warning(text: String) do
191 emit_text "<p class='text-warning'>Warning: {text}</p>"
192 end
193
194 # Write an error in the output
195 fun write_error(text: String) do
196 emit_text "<p class='text-danger'>Error: {text}</p>"
197 end
198
199 # Write a link to a mentity in the output
200 fun write_mentity_link(mentity: MEntity, text: nullable String) do
201 var link = mentity.web_url
202 var name = text or else mentity.name
203 var mdoc = mentity.mdoc_or_fallback
204 var comment = null
205 if mdoc != null then comment = mdoc.synopsis
206 decorator.add_link(self, link, name, comment)
207 end
208
209 # Write a mentity list in the output
210 fun write_mentity_list(mentities: Collection[MEntity]) do
211 add "<ul>"
212 for mentity in mentities do
213 var mdoc = mentity.mdoc_or_fallback
214 add "<li>"
215 write_mentity_link(mentity)
216 if mdoc != null then
217 add " - "
218 emit_text mdoc.synopsis
219 end
220 add "</li>"
221 end
222 add "</ul>"
223 end
224 end
225
226 redef class DocCommand
227
228 # Emit the HTML related to the execution of this doc command
229 fun render(v: MarkdownProcessor, token: TokenWikiLink, model: ModelView) do
230 v.write_error("not yet implemented command `{token.link or else "null"}`")
231 end
232 end
233
234 redef class CommentCommand
235 redef fun render(v, token, model) do
236 var name = arg
237 var mentity = v.find_mentity(model, name)
238 if mentity == null then return
239 var mdoc = mentity.mdoc_or_fallback
240 if mdoc == null then
241 v.write_warning("no MDoc for mentity `{name}`")
242 return
243 end
244 v.add "<h3>"
245 if not opts.has_key("no-link") then
246 v.write_mentity_link(mentity)
247 end
248 if not opts.has_key("no-link") and not opts.has_key("no-synopsis") then
249 v.add " - "
250 end
251 if not opts.has_key("no-synopsis") then
252 v.emit_text mdoc.html_synopsis.write_to_string
253 end
254 v.add "</h3>"
255 if not opts.has_key("no-comment") then
256 v.add v.process(mdoc.comment).write_to_string
257 end
258 end
259 end
260
261 redef class ListCommand
262 redef fun render(v, token, model) do
263 var name = arg
264 var mentity = v.find_mentity(model, name)
265 if mentity == null then return
266 if mentity isa MPackage then
267 v.write_mentity_list(mentity.mgroups)
268 else if mentity isa MGroup then
269 var res = new Array[MEntity]
270 res.add_all mentity.in_nesting.smallers
271 res.add_all mentity.mmodules
272 v.write_mentity_list(res)
273 else if mentity isa MModule then
274 v.write_mentity_list(mentity.mclassdefs)
275 else if mentity isa MClass then
276 v.write_mentity_list(mentity.collect_intro_mproperties(model))
277 else if mentity isa MClassDef then
278 v.write_mentity_list(mentity.mpropdefs)
279 else if mentity isa MProperty then
280 v.write_mentity_list(mentity.mpropdefs)
281 else
282 v.write_error("no list found for name `{name}`")
283 end
284 end
285 end
286
287 redef class CodeCommand
288 redef fun render(v, token, model) do
289 var name = arg
290 var mentity = v.find_mentity(model, name)
291 if mentity == null then return
292 if mentity isa MClass then mentity = mentity.intro
293 if mentity isa MProperty then mentity = mentity.intro
294 var source = render_source(mentity, v.decorator.as(NitwebDecorator).modelbuilder)
295 if source == null then
296 v.write_error("no source for MEntity `{name}`")
297 return
298 end
299 v.add "<pre>"
300 v.add source
301 v.add "</pre>"
302 end
303
304 # Highlight `mentity` source code.
305 private fun render_source(mentity: MEntity, modelbuilder: ModelBuilder): nullable HTMLTag do
306 var node = modelbuilder.mentity2node(mentity)
307 if node == null then return null
308 var hl = new HighlightVisitor
309 hl.enter_visit node
310 return hl.html
311 end
312 end
313
314 redef class GraphCommand
315 redef fun render(v, token, model) do
316 var name = arg
317 var mentity = v.find_mentity(model, name)
318 if mentity == null then return
319 var g = new InheritanceGraph(mentity, model)
320 var pdepth = if opts.has_key("pdepth") and opts["pdepth"].is_int then
321 opts["pdepth"].to_i else 3
322 var cdepth = if opts.has_key("cdepth") and opts["cdepth"].is_int then
323 opts["cdepth"].to_i else 3
324 v.add g.draw(pdepth, cdepth).to_svg
325 end
326 end
327
328 redef class Text
329 # Read `self` between `nstart` and `nend` (excluded) and writte chars to `out`.
330 private fun read(out: FlatBuffer, nstart, nend: Int): Int do
331 var pos = nstart
332 while pos < length and pos < nend do
333 out.add self[pos]
334 pos += 1
335 end
336 if pos == length then return -1
337 return pos
338 end
339 end