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