model: remove a warning :p
[nit.git] / src / doc / doc_phases / doc_readme.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 # This phase parses README files.
16 module doc_readme
17
18 import markdown::decorators
19 intrude import markdown::wikilinks
20 import doc_commands
21 import doc_down
22 import doc_intros_redefs
23 import model::model_index
24
25 # Generate content of `ReadmePage`.
26 #
27 # This phase extracts the structure of a `ReadmePage` from the markdown content
28 # of the README file.
29 # It also resolves Wikilinks and commands.
30 class ReadmePhase
31 super DocPhase
32
33 redef fun apply do
34 for page in doc.pages.values do page.build_content(self, doc)
35 end
36
37 # Display a warning about something wrong in the readme file.
38 fun warning(location: nullable MDLocation, page: ReadmePage, message: String) do
39 var loc = null
40 if location != null then
41 loc = location.to_location(page.mentity.mdoc.location.file)
42 end
43 ctx.warning(loc, "readme-warning", message)
44 end
45 end
46
47 redef class DocPage
48 # Build content of `ReadmePage` based on the content of the readme file.
49 private fun build_content(v: ReadmePhase, doc: DocModel) do end
50 end
51
52 redef class ReadmePage
53 redef fun build_content(v, doc) do
54 if mentity.mdoc == null then
55 v.warning(null, self, "Empty README for group `{mentity}`")
56 return
57 end
58 var proc = new MarkdownProcessor
59 proc.emitter = new ReadmeMdEmitter(proc, self, v)
60 proc.emitter.decorator = new ReadmeDecorator
61 var md = mentity.mdoc.content.join("\n")
62 proc.process(md)
63 end
64 end
65
66 # Markdown emitter used to produce the `ReadmeArticle`.
67 class ReadmeMdEmitter
68 super MarkdownEmitter
69
70 # Readme page being decorated.
71 var page: ReadmePage
72
73 # Phase used to access doc model and toolcontext.
74 var phase: ReadmePhase
75
76 init do open_article
77
78 # Push the article template on top of the buffer stack.
79 #
80 # Subsequent markdown writting will be done in the article template.
81 #
82 # See `ReadmeArticle::md`.
83 private fun push_article(article: ReadmeArticle) do
84 buffer_stack.add article.md
85 end
86
87 private var context = new Array[DocComposite]
88
89 # Creates a new ReadmeSection in `self.toc.page`.
90 #
91 # Called from `add_headline`.
92 private fun open_section(lvl: Int, title: String) do
93 var section = new ReadmeSection(title.escape_to_c, title, lvl, processor)
94 if current_section == null then
95 page.root.add_child(section)
96 else
97 current_section.add_child(section)
98 end
99 current_section = section
100 context.add section
101 end
102 private var current_section: nullable ReadmeSection is noinit
103
104 # Close the current section.
105 #
106 # Ensure `context.last isa ReadmeSection`.
107 private fun close_section do
108 assert context.last isa ReadmeSection
109 context.pop
110 if context.is_empty then
111 current_section = null
112 else
113 current_section = context.last.as(ReadmeSection)
114 end
115 end
116
117 # Add an article at current location.
118 #
119 # This closes the current article, inserts `article` then opens a new article.
120 private fun add_article(article: DocArticle) do
121 close_article
122 if current_section == null then
123 page.root.add_child(article)
124 else
125 current_section.add_child(article)
126 end
127 open_article
128 end
129
130 # Creates a new ReadmeArticle in `self.toc.page`.
131 #
132 # Called from `add_headline`.
133 private fun open_article do
134 var section: DocComposite = page.root
135 if current_section != null then section = current_section.as(not null)
136 var article = new ReadmeArticle("mdarticle-{section.children.length}", null, processor)
137 section.add_child(article)
138 context.add article
139 push_article article
140 end
141
142 # Close the current article.
143 #
144 # Ensure `context.last isa ReadmeArticle`.
145 fun close_article do
146 assert context.last isa ReadmeArticle
147 context.pop
148 pop_buffer
149 end
150
151 # Find mentities matching `query`.
152 fun find_mentities(query: String): Array[MEntity] do
153 # search MEntities by full_name
154 var mentity = phase.doc.mentity_by_full_name(query)
155 if mentity != null then return [mentity]
156 # search MEntities by name
157 return phase.doc.mentities_by_name(query)
158 end
159
160 # Suggest mentities based on `query`.
161 fun suggest_mentities(query: String): Array[MEntity] do
162 return phase.doc.find(query, 3)
163 end
164
165 # Display a warning message with suggestions.
166 fun warn(token: TokenWikiLink, message: String, suggest: nullable Array[MEntity]) do
167 var msg = new Buffer
168 msg.append message
169 if suggest != null and suggest.not_empty then
170 msg.append " (suggestions: "
171 var i = 0
172 for s in suggest do
173 msg.append "`{s.full_name}`"
174 if i < suggest.length - 1 then msg.append ", "
175 i += 1
176 end
177 msg.append ")"
178 end
179 phase.warning(token.location, page, msg.write_to_string)
180 end
181 end
182
183 # MarkdownDecorator used to decorated the Readme file with links between doc entities.
184 class ReadmeDecorator
185 super MdDecorator
186
187 redef type EMITTER: ReadmeMdEmitter
188
189 redef fun add_headline(v, block) do
190 var txt = block.block.first_line.value
191 var lvl = block.depth
192 if not v.context.is_empty then
193 v.close_article
194 while v.current_section != null do
195 if v.current_section.depth < lvl then break
196 v.close_section
197 end
198 end
199 v.open_section(lvl, txt)
200 v.open_article
201 end
202
203 redef fun add_wikilink(v, token) do
204 var link = token.link.to_s
205 var cmd = new DocCommand(link)
206 if cmd isa UnknownCommand then
207 # search MEntities by name
208 var res = v.find_mentities(link.to_s)
209 # no match, print warning and display wikilink as is
210 if res.is_empty then
211 v.warn(token, "Link to unknown entity `{link}`", v.suggest_mentities(link.to_s))
212 super
213 else
214 add_mentity_link(v, res.first, token.name, token.comment)
215 end
216 return
217 end
218 cmd.render(v, token)
219 end
220
221 # Renders a link to a mentity.
222 private fun add_mentity_link(v: EMITTER, mentity: MEntity, name, comment: nullable Text) do
223 # TODO real link
224 var link = mentity.full_name
225 if name == null then name = mentity.name
226 if comment == null and mentity.mdoc != null then
227 comment = mentity.mdoc.synopsis
228 end
229 add_link(v, link, name, comment)
230 end
231 end
232
233 redef interface DocCommand
234
235 # Render the content of the doc command.
236 fun render(v: ReadmeMdEmitter, token: TokenWikiLink) is abstract
237 end
238
239 redef class ArticleCommand
240 redef fun render(v, token) do
241 var string = args.first
242 var res = v.find_mentities(string)
243 if res.is_empty then
244 v.warn(token,
245 "Try to include documentation of unknown entity `{string}`",
246 v.suggest_mentities(string))
247 return
248 end
249 v.add_article new DocumentationArticle("readme", "Readme", res.first)
250 end
251 end
252
253 redef class ListCommand
254 redef fun render(v, token) do
255 var string = args.first
256 var res = v.find_mentities(string)
257 if res.is_empty then
258 v.warn(token,
259 "Try to include article of unknown entity `{string}`",
260 v.suggest_mentities(string))
261 return
262 end
263 if res.length > 1 then
264 v.warn(token, "Conflicting article for `{args.first}`", res)
265 end
266 var mentity = res.first
267 if mentity isa MModule then
268 v.add_article new MEntitiesListArticle("Classes", null, mentity.mclassdefs)
269 else if mentity isa MClass then
270 var mprops = mentity.collect_intro_mproperties(mentity.public_view)
271 v.add_article new MEntitiesListArticle("Methods", null, mprops.to_a)
272 else if mentity isa MClassDef then
273 v.add_article new MEntitiesListArticle("Methods", null, mentity.mpropdefs)
274 end
275 end
276 end
277
278
279 # A section found in a README.
280 #
281 # Produced by markdown headlines like `## Section 1.1`.
282 class ReadmeSection
283 super DocSection
284
285 # The depth is based on the markdown headline depth.
286 redef var depth
287
288 # Markdown processor used to process the section title.
289 var markdown_processor: MarkdownProcessor
290
291 redef var is_hidden = false
292 end
293
294 # An article found in a README file.
295 #
296 # Basically, everything found in a README that is not a headline.
297 class ReadmeArticle
298 super DocArticle
299
300 # Markdown processor used to process the article content.
301 var markdown_processor: MarkdownProcessor
302
303 # Markdown content of this article extracted from the README file.
304 var md = new FlatBuffer
305
306 redef fun is_hidden do return super and md.trim.is_empty
307 end
308
309 # Documentation Article to introduce from the directive `doc: Something`.
310 #
311 # TODO merge with DefinitionArticle once the html is simplified
312 class DocumentationArticle
313 super MEntityArticle
314
315 redef var is_hidden = false
316 end
317
318 redef class MDLocation
319 # Translate a Markdown location in Nit location.
320 private fun to_location(file: nullable SourceFile): Location do
321 return new Location(file, line_start, line_end, column_start, column_end)
322 end
323 end