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